feat: Go to Declaration for Qiq helper calls (1.x HelperLocator + 2.x/3.x Helpers)#17
Merged
Conversation
Resolve `{{ helperName(...) }}` / `{{ $this->helperName(...) }}` in
templates to the PHP class registered via HelperLocator::set(). Helper
name -> class mapping is built by QiqHelperRegistry, which scans the
bootstrap file(s) nominated in Settings > Languages & Frameworks > Qiq
Templates (the closure's declared return type or `return new X(...)`).
- QiqHelperGotoDeclarationHandler: navigation via gotoDeclarationHandler
(function/method call names never aggregate contributed references, so
a PsiReferenceContributor would be ignored).
- QiqHelperInspectionSuppressor: suppress PhpUndefinedFunction/Method
warnings for resolvable helper calls inside Qiq files (mirrors Blade).
- Settings UI: file-chooser list for helper bootstrap files, stored
relative to the project base dir when possible.
Also fix a RAW_CONTENT injection bug: `{{= asset(...) }}` had its leading
`a` stripped as the `{{a }}` escape modifier (corrupting the call to
`sset(...)`). Helper names starting with h/a/j/u/c now pass through
verbatim; the legacy text-modifier strip requires a standalone modifier
byte followed by whitespace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds PhpStorm navigation and inspection support for Qiq “helper calls” by scanning user-configured PHP bootstrap files that register helpers via HelperLocator::set(), and fixes a RAW-content injection regression for helper names starting with escape-modifier letters.
Changes:
- Implement Go to Declaration for Qiq helper calls via a
GotoDeclarationHandler, backed by a newQiqHelperRegistryscanner. - Add settings UI + persistence for configuring helper bootstrap PHP files, and invalidate the helper scan cache on apply.
- Fix
{{= ... }}RAW echo injection so helper names likeasset(...)aren’t corrupted by legacy escape-modifier stripping; add regression tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/test/kotlin/io/github/jingu/idea_qiq_plugin/inject/QiqPhpInjectorTest.kt | Adds regression coverage for RAW echo helper calls starting with escape letters. |
| src/test/kotlin/io/github/jingu/idea_qiq_plugin/helper/QiqHelperRegistryTest.kt | Adds unit tests for helper name → class scanning across multiple PHP factory shapes. |
| src/main/resources/META-INF/plugin.xml | Registers the new goto-declaration handler and PHP inspection suppressor. |
| src/main/resources/messages/QiqBundle.properties | Adds user-facing strings for helper bootstrap file settings UI. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/settings/QiqSettingService.kt | Persists helper bootstrap file list and resolves configured paths to VirtualFiles. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/settings/QiqProjectConfigurable.kt | Adds UI for selecting and storing helper bootstrap files (multi-select). |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/navigation/QiqHelperGotoDeclarationHandler.kt | Implements helper-call navigation in injected PHP via GotoDeclarationHandler. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/lang/QiqInjectionSupport.kt | Centralizes “is this element in a Qiq template (incl. injected PHP)” detection. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/inspection/QiqHelperInspectionSuppressor.kt | Suppresses undefined function/method inspections for resolvable helper calls in Qiq. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/inject/QiqPhpInjector.kt | Fixes RAW-content echo injection to avoid stripping leading helper-name characters. |
| src/main/kotlin/io/github/jingu/idea_qiq_plugin/helper/QiqHelperRegistry.kt | Adds the helper registry and static scanner with caching keyed by file modification stamps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add navigation + warning suppression for the Qiq 2.x/3.x official custom helper style: public methods on a subclass of Qiq\Helpers (typically extending Qiq\Helper\Html\HtmlHelpers), passed via Template::new(helpers:). QiqHelpersClassResolver walks Qiq\Helpers subclasses via PhpIndex and indexes their public, non-static, non-magic own methods (helperName -> Method), cached against PsiModificationTracker. Library classes under the \Qiq\ namespace are skipped — their built-ins resolve through the qiq_runtime.php stub. No user configuration is required; it self-gates (a 1.x project has no \Qiq\Helpers, so the walk is empty). QiqHelperGotoDeclarationHandler now offers both resolution paths (1.x HelperLocator classes and 2.x/3.x Helpers methods); the inspection suppressor likewise suppresses undefined-function/method warnings for either. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the per-class helper-method discriminator into a pure QiqHelpersClassResolver.helperMethodsOf(PhpClass) companion function and unit-test it against in-memory PHP PSI: public/non-static/non-magic own methods of a user subclass are kept, members and classes under the \Qiq\ namespace are excluded. The PhpIndex processAllSubclasses walk stays a thin platform call; testing it would require an indexed fixture project (BasePlatformTestCase), which trips the platform light-project leak detector under the JUnit vintage engine, so the index-backed traversal is left to manual verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- QiqHelperRegistry.bodyNewExpressionFqn: only treat a `new X(...)` as the factory's return when it is the *direct* returned value (return argument for closures, direct body child for arrow functions). Avoids false positives such as `return foo(new X())` or a `new` assigned to a local but not returned. - Drop unused VirtualFile import. - File chooser filter: call equals on the non-null literal so the nullable VirtualFile.extension can't be dereferenced. - Add regression tests for the nested/non-returned new-expression cases. 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
Cmd/Ctrl+click on a Qiq helper call (
{{ helperName(...) }}or{{ $this->helperName(...) }}) now navigates to the helper's PHP declaration. Covers both Qiq helper styles:QiqHelperRegistryscans the bootstrap file(s) nominated in Settings > Languages & Frameworks > Qiq Templates, reading each$locator->set('name', closure)and resolving the target from the closure's declared return type or areturn new ClassName(...)statement → navigates to theQiq\Helper\<X>::__invoke/class.QiqHelpersClassResolverdiscovers every project-defined subclass ofQiq\Helpers(the official custom-helper style: public methods on a class extendingQiq\Helper\Html\HtmlHelpers, passed viaTemplate::new(helpers: ...)) throughPhpIndexand resolvesname→ public method. No configuration needed; self-gates (a 1.x project has no\Qiq\Helpers). Library classes under\Qiq\are skipped — built-ins already resolve via the bundled stub.Both paths also feed
QiqHelperInspectionSuppressor, which suppresses PhpStorm's "undefined function/method" warnings for resolvable helper calls inside Qiq files (mirroring the bundled Blade support).Settings UI gains a file-chooser list (multi-select,
.phponly) for the 1.x helper bootstrap files; paths are stored relative to the project base dir when possible.Why GotoDeclarationHandler (not PsiReferenceContributor)
Function/method call names carry their own resolution and never aggregate contributed references, so a
PsiReferenceContributoris ignored for them.gotoDeclarationHandleris the correct extension point and works inside injected PHP.Injection fix (RAW_CONTENT)
{{= asset(...) }}previously had its leadingastripped as the{{a }}escape modifier, corrupting the injected call tosset(...). Helper names starting withh/a/j/u/c(asset, currentUrl, upper, …) now pass through verbatim:RAW_CONTENT({{= }}) is emitted as a plain<?= ... ?>echo.Test plan
./gradlew test— all green (QiqHelperRegistryTest,QiqPhpInjectorTestincl. newrawEchoHelperCallStartingWithEscapeLetterIsNotStripped)./gradlew buildPluginNotes / limitations
set/setFactory/registerwith an inline closure/arrow whose target is a declared return type ornew X(...). Dynamic names, callable-array/class-string/variable factories, and indirect ($container->make(...)) factories are not resolved.Qiq\Helperssubclasses, cached againstPsiModificationTracker.🤖 Generated with Claude Code