Skip to content

feat: Go to Declaration for Qiq helper calls (1.x HelperLocator + 2.x/3.x Helpers)#17

Merged
jingu merged 4 commits into
developfrom
feat/helper-code-jump
May 29, 2026
Merged

feat: Go to Declaration for Qiq helper calls (1.x HelperLocator + 2.x/3.x Helpers)#17
jingu merged 4 commits into
developfrom
feat/helper-code-jump

Conversation

@jingu
Copy link
Copy Markdown
Owner

@jingu jingu commented May 29, 2026

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:

  • Qiq 1.x — HelperLocator (manual config): QiqHelperRegistry scans 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 a return new ClassName(...) statement → navigates to the Qiq\Helper\<X>::__invoke/class.
  • Qiq 2.x/3.x — Helpers subclass (automatic): QiqHelpersClassResolver discovers every project-defined subclass of Qiq\Helpers (the official custom-helper style: public methods on a class extending Qiq\Helper\Html\HtmlHelpers, passed via Template::new(helpers: ...)) through PhpIndex and resolves name → 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, .php only) 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 PsiReferenceContributor is ignored for them. gotoDeclarationHandler is the correct extension point and works inside injected PHP.

Injection fix (RAW_CONTENT)

{{= asset(...) }} previously had its leading a stripped as the {{a }} escape modifier, corrupting the injected call to sset(...). Helper names starting with h/a/j/u/c (asset, currentUrl, upper, …) now pass through verbatim:

  • RAW_CONTENT ({{= }}) is emitted as a plain <?= ... ?> echo.
  • The legacy text-modifier strip now requires a standalone modifier byte followed by whitespace.

Test plan

  • ./gradlew test — all green (QiqHelperRegistryTest, QiqPhpInjectorTest incl. new rawEchoHelperCallStartingWithEscapeLetterIsNotStripped)
  • ./gradlew buildPlugin
  • Manual verification in PhpStorm against a real BEAR.Sunday/Qiq 1.x project: helper Cmd+click jumps to the helper class; the leading-letter corruption is fixed; undefined-function warning suppressed for registered helpers
  • Manual verification against a Qiq 2.x/3.x project (Helpers subclass auto-discovery) — pending

Notes / limitations

  • 1.x resolver handles set/setFactory/register with an inline closure/arrow whose target is a declared return type or new X(...). Dynamic names, callable-array/class-string/variable factories, and indirect ($container->make(...)) factories are not resolved.
  • 2.x/3.x resolver indexes public, non-static, non-magic own methods of Qiq\Helpers subclasses, cached against PsiModificationTracker.
  • The PhpIndex-based 2.x/3.x discovery is not covered by an automated test (needs an indexed fixture project); verified by compile/build and to be confirmed manually.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 29, 2026 05:37
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 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 new QiqHelperRegistry scanner.
  • 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 like asset(...) 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.

Comment thread src/main/kotlin/io/github/jingu/idea_qiq_plugin/helper/QiqHelperRegistry.kt Outdated
Comment thread src/main/kotlin/io/github/jingu/idea_qiq_plugin/helper/QiqHelperRegistry.kt Outdated
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>
@jingu jingu changed the title feat: Go to Declaration for Qiq helper calls feat: Go to Declaration for Qiq helper calls (1.x HelperLocator + 2.x/3.x Helpers) May 29, 2026
jingu and others added 2 commits May 29, 2026 15:02
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>
@jingu jingu merged commit ae88882 into develop May 29, 2026
1 check passed
@jingu jingu deleted the feat/helper-code-jump branch May 29, 2026 06:37
@jingu jingu mentioned this pull request May 29, 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