feat: add Strict Types project setting for Qiq templates#15
Merged
Conversation
PR #12 noted that scalar literal misuses in escape directives — e.g. `{{h true }}`, `{{h 123 }}`, `{{h null }}` — do not surface as warnings because PhpStorm's Type Compatibility inspection allows implicit scalar→string casts unless the caller declares `strict_types=1`. The stub file's own declaration has no effect since strict_types is per-caller-file in PHP. This change adds a project-level opt-in setting that prepends `<?php declare(strict_types=1); ?>` to each Qiq template's injected PHP. The setting lives under Settings > Languages & Frameworks > Qiq Templates and is off by default to match Qiq's runtime behaviour. Implementation: - QiqSettingsService.State gains `enableStrictTypes: Boolean = false` with accessor / mutator methods so the configurable can bind to it through PersistentStateComponent serialization. - QiqPhpInjector.ensureInjectionPlan post-processes the fragment list via applyStrictTypesPreludeIfEnabled, which prepends the declare to the first fragment's prefix. The virtual injected file is the concatenation of fragment prefixes/hosts/suffixes, so placing the declare at the head of the first prefix puts it as the very first statement of the virtual file — which is where PHP requires it. - QiqProjectConfigurable surfaces the setting via the modern Kotlin UI DSL (BoundSearchableConfigurable + bindSelected) and is registered as a projectConfigurable under the "language" parent so it appears under Languages & Frameworks. Tests cover three cases: setting OFF preserves the existing prefix, setting ON prepends the declare, and setting ON only touches the first of multiple fragments. Verified in PhpStorm 2026.1 on a Qiq 1.x project: with the setting on, `{{h true }}`, `{{h 123 }}`, `{{h 3.14 }}` and `{{h strpos(...) }}` all surface as "in strict type matching, string expected, X given" warnings backed by the corresponding ERROR-level diagnostic. With the setting off, only the always-failing array/object cases warn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an opt-in project-level Strict Types setting for Qiq templates, intended to prepend declare(strict_types=1) to injected PHP so PhpStorm can report stricter scalar type mismatches in escape directives.
Changes:
- Adds persisted
enableStrictTypesstate and accessors. - Adds a Qiq Templates project settings page with a strict-types checkbox.
- Prepends the strict-types prelude to the first PHP injection fragment and adds injector tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/main/kotlin/io/github/jingu/idea_qiq_plugin/settings/QiqSettingService.kt |
Adds persisted strict-types setting state and accessors. |
src/main/kotlin/io/github/jingu/idea_qiq_plugin/settings/QiqProjectConfigurable.kt |
Adds the project settings UI for toggling strict-types injection. |
src/main/kotlin/io/github/jingu/idea_qiq_plugin/inject/QiqPhpInjector.kt |
Applies the strict-types prelude to generated PHP injection fragments. |
src/main/resources/META-INF/plugin.xml |
Registers the Qiq Templates project configurable. |
src/main/resources/messages/QiqBundle.properties |
Adds localized settings labels and help text. |
src/test/kotlin/io/github/jingu/idea_qiq_plugin/inject/QiqPhpInjectorTest.kt |
Adds tests for default/off, enabled, and first-fragment-only prelude behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- QiqProjectConfigurable: rebind the strict-types checkbox through the public is/setStrictTypesEnabled accessors instead of the private `state` property. The previous form happened to compile thanks to the synthesized PersistentStateComponent getter, but it leaked the storage shape into the configurable. - QiqPhpInjector: extend the InjectionPlan cache key with strictTypesEnabled. The plan depends on the project-level setting, so toggling the checkbox on an already-open file (no edit, no modification-stamp change) used to reuse the stale plan until the next reparse. Now the cache is keyed by both modificationStamp and the strict-types setting. - QiqPhpInjectorTest: regression test togglingStrictTypesSettingInvalidatesCachedInjectionPlan that flips the setting OFF → ON → OFF on the same file and asserts the prelude appears and disappears each time without an intervening edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #12 noted a known limitation: scalar literal misuses in escape directives — for example
{{h true }},{{h 123 }},{{h null }}— do not surface as warnings because PhpStorm's Type Compatibility inspection allows implicit scalar→string casts unless the caller declaresstrict_types=1. Puttingdeclare(strict_types=1)in the stub itself has no effect because PHP appliesstrict_typesper-caller-file.This PR adds an opt-in project-level setting that prepends
<?php declare(strict_types=1); ?>to each Qiq template's injected PHP, so PhpStorm runs the entire virtual injected file under strict mode and the previously-unreachable scalar literal mismatches become visible warnings.Settings UI
Settings > Languages & Frameworks > Qiq Templates— single checkbox:Implementation
QiqSettingsService.StategainsenableStrictTypes: Boolean = falsewithis...Enabled()/set...Enabled(...)accessors so the configurable can bind throughPersistentStateComponent.QiqPhpInjector.ensureInjectionPlanpost-processes the fragment list viaapplyStrictTypesPreludeIfEnabled, which prepends the declare to the first fragment's prefix. The virtual injected PHP file is the concatenation of fragmentprefix + host + suffixchunks, so placing the declare at the head of the first prefix puts it as the very first statement of the virtual file — where PHP's spec requiresdeclare(strict_types=...)to be.QiqProjectConfigurableis aBoundSearchableConfigurableusing the Kotlin UI DSL (panel { row { checkBox(...).bindSelected(state::enableStrictTypes) } }) and is registered as aprojectConfigurableunder thelanguageparent so it appears under Languages & Frameworks.Tests
QiqPhpInjectorTestgains three new cases:strictTypesPreludeIsAbsentByDefault— the existing prefix is untouched when the setting is off.strictTypesPreludeIsPrependedWhenEnabled— when on, the first fragment's prefix begins with<?php declare(strict_types=1); ?>and the original escape-routing prefix (<?= \QiqRuntimeFunctions(Strict)?::h() still follows.strictTypesPreludeOnlyTouchesFirstFragment— given three hosts ({{ foreach ... }},{{h ... }},{{ endforeach }}), exactly one fragment carries the prelude, and it is the first.Manual verification (PhpStorm 2026.1, Qiq 1.x project)
With the setting on, every previously-silent scalar case now produces a
WARNING(with the "in strict type matching" wording — direct evidence PhpStorm is honouring the injected declare) plus a correspondingERROR:{{h 'hello' }}{{h true }}{{h 123 }}{{h 3.14 }}{{h null }}{{h strpos('a','b') }}(false|int){{h [] }}{{h new stdClass() }}Caveats
Test plan
./gradlew test— existing + new tests pass./gradlew buildPlugin— produces a valid distribution🤖 Generated with Claude Code