Skip to content

feat: add Strict Types project setting for Qiq templates#15

Merged
jingu merged 2 commits into
developfrom
feat/strict-types-setting
May 27, 2026
Merged

feat: add Strict Types project setting for Qiq templates#15
jingu merged 2 commits into
developfrom
feat/strict-types-setting

Conversation

@jingu
Copy link
Copy Markdown
Owner

@jingu jingu commented May 27, 2026

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 declares strict_types=1. Putting declare(strict_types=1) in the stub itself has no effect because PHP applies strict_types per-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:

Inject declare(strict_types=1) into Qiq templates

Surfaces scalar (bool/int/null) misuses in {{h ...}} / {{a ...}} / etc. as type warnings. Off by default; turn on to match a project that compiles its rendered PHP under strict types.

Implementation

  • QiqSettingsService.State gains enableStrictTypes: Boolean = false with is...Enabled() / set...Enabled(...) accessors so the configurable can bind through PersistentStateComponent.
  • QiqPhpInjector.ensureInjectionPlan post-processes the fragment list via applyStrictTypesPreludeIfEnabled, which prepends the declare to the first fragment's prefix. The virtual injected PHP file is the concatenation of fragment prefix + host + suffix chunks, 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 requires declare(strict_types=...) to be.
  • QiqProjectConfigurable is a BoundSearchableConfigurable using the Kotlin UI DSL (panel { row { checkBox(...).bindSelected(state::enableStrictTypes) } }) and is registered as a projectConfigurable under the language parent so it appears under Languages & Frameworks.

Tests

QiqPhpInjectorTest gains 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 corresponding ERROR:

Case Setting OFF Setting ON
{{h 'hello' }} no warning no warning
{{h true }} no warning string expected, true given
{{h 123 }} no warning string expected, int given
{{h 3.14 }} no warning string expected, float given
{{h null }} no warning string expected, null given
{{h strpos('a','b') }} (false|int) no warning string expected, false|int given
{{h [] }} array warning array warning
{{h new stdClass() }} stdClass warning stdClass warning

Caveats

  • The prelude affects all injected PHP in the file, not just escape directives. That is intentional — a template that opts into strict types should be strict everywhere — but worth noting.
  • This is a project-level setting; it does not roam with each file. If a project mixes strict and non-strict templates, users will need to keep that distinction in their PHP rendering layer (which is what determines runtime behaviour anyway).

Test plan

  • ./gradlew test — existing + new tests pass
  • ./gradlew buildPlugin — produces a valid distribution
  • Real-project: setting visible in Settings UI, persists across restarts
  • Real-project: scalar literal warnings appear when on, disappear when off; array / object warnings remain in both states

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 27, 2026 01:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 enableStrictTypes state 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.

Comment thread src/main/kotlin/io/github/jingu/idea_qiq_plugin/inject/QiqPhpInjector.kt Outdated
- 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>
@jingu jingu merged commit 5dc42a9 into develop May 27, 2026
1 check passed
@jingu jingu deleted the feat/strict-types-setting branch May 27, 2026 02:06
@jingu jingu mentioned this pull request May 27, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants