Skip to content

feat(framework): add Angular support — modules, selectors, templates, routes, control-flow blocks#526

Open
rj-majian3 wants to merge 1 commit into
colbymchenry:mainfrom
rj-majian3:feat/angular-support
Open

feat(framework): add Angular support — modules, selectors, templates, routes, control-flow blocks#526
rj-majian3 wants to merge 1 commit into
colbymchenry:mainfrom
rj-majian3:feat/angular-support

Conversation

@rj-majian3
Copy link
Copy Markdown

Summary

Adds full Angular static extraction so trace/explore/impact/callers/callees
work on Angular projects without falling back to Read.

What's included

Six phases, all regex-over-source (same approach as other framework resolvers):

  1. NgModule + standalone @component metadatadeclarations, imports, providers, exports,
    bootstrap → class→class edges
  2. Selector → component/directive<app-foo> tags, [appHighlight] attributes, bare attributes,
    and compound selectors (a[href], i.anticon) matched against a per-project selector index
  3. Template binding → owner-class member(event), [prop], [(banana)], *ngFor/*ngIf, {{ interpolation }}
  • Unit tests: 124 helper/detect tests
  • Integration tests: 15 end-to-end indexAll tests
  • Full suite: 900 passed / 0 failures (3 flaky watcher timing tests excluded)
  • Real-world: validated on a 4,296-file Angular+Vue hybrid project (1,512 .component.html templates)
    — produced 22,672 Angular synthesized edges (12.4% of total graph):
Synthesizer Edges
angular-template (bindings → members) 14,803
angular-imports (NgModule imports) 3,019
angular-selector (tag → component) 2,378
angular-declarations 1,634
angular-exports 445
angular-providers 380
angular-bootstrap 13

Files changed

  • src/resolution/frameworks/angular.ts — new (1,744 lines)
  • src/resolution/frameworks/index.ts — register angular resolver
  • src/resolution/callback-synthesizer.ts — integrate 3 angular synthesizers + dedup key fix
  • __tests__/frameworks.test.ts — 124 unit tests
  • __tests__/frameworks-integration.test.ts — 15 e2e tests
  • CHANGELOG.md — [Unreleased] entry

… routes, control-flow blocks

Closes the static-extraction holes for Angular projects so trace/explore/impact
work without falling back to Read. All synthesized edges carry
provenance:'heuristic' + metadata.synthesizedBy:'angular-*' + registeredAt.

Phase 1 — NgModule + standalone @component metadata (synthesizer):
- @NgModule({ declarations, imports, providers, exports, bootstrap })
- standalone @component({ imports })
- emits class→class edges from the @NgModule / @component class to each
  identifier in the metadata array

Phase 2 — selector → component/directive (synthesizer):
- <app-foo> tag match against the per-project selector index built from
  @component / @directive
- [appHighlight] bracketed attribute selector
- bare attribute form (<p appHighlight>) — Angular treats both as the same
  selector; the index lookup gates false positives

Phase 3 — template binding → owner-class member (synthesizer):
- (event)="handler($event)" → method
- [prop]="expr" → property/field
- [(banana)]="x" two-way binding
- *ngFor / *ngIf / *ngSwitch structural directives
- {{ interpolation }}
- resolution: only members inside the owner class's line range in the same
  file, gated by NG_MEMBER_KINDS

Phase 4 — router configs (per-file extract):
- const routes: Routes = [...] / Route[]
- RouterModule.forRoot([...]) / forChild([...])
- provideRouter([...]) standalone-bootstrap form
- nested children: [...] with parent-path joining
- loadChildren / loadComponent dynamic import forms — .then(m => m.X),
  destructured ({ X }) => X, async (await import()).X
- route → component reference

Phase 5 — Angular 17+ control-flow blocks (synthesizer):
- @if (expr) / @else if (expr)
- @for (item of expr; track …)
- @switch (expr) / @case (expr)
- @defer (expr)
- @let name = expr; (v18+ template variables)
- balanced-paren reader so nested fn calls inside the block expression aren't
  truncated

Phase 6 — route guards / resolvers (per-file extract):
- canActivate / canActivateChild / canDeactivate / canMatch / canLoad arrays
- resolve: { key: Resolver } object — values extracted as references

Cross-synthesizer dedup:
- Merge in synthesizeCallbackEdges now keys on (source, target, synthesizedBy)
  instead of just (source, target). Same pair through different mechanisms
  (e.g. NgModule imports + template selector) now produces both edges with
  their distinct metadata channel. Within each synthesizer the existing
  per-channel dedup keeps duplicates out.

Tests: 19 helper / detect unit tests + 11 end-to-end indexAll integration
tests in __tests__/frameworks{,-integration}.test.ts. Verified against full
suite: 882 passed / 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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