feat(neo4j): project decorators, matching python - #143
Conversation
Decorators were captured as structured `TSDecorator` in the JSON from the start —
name, checker-resolved `qualified_name`, positional and keyword arguments, spans —
on types, callables, fields and parameters. None of it reached Neo4j: the whole
projection mentioned decorators once, in a comment. A query could see `@Controller`
in analysis.json and not in the graph.
Mirror python's `_project_decorator`:
- `:TSDecorator` merged on the resolved `qualified_name` when there is one, so
`@Get('/')` and `@Get('/:id')` collapse to a single node rather than two spellings
of one decorator.
- `TS_DECORATED_BY` from the decorated node to it, carrying the per-application
facts: `positional_arguments` (raw source fragments) and `keyword_arguments_json`
(sorted-key JSON, since Neo4j has no map property type — python encodes the same
field the same way, so the two projections stay diffable).
Like python's, the node carries no `_module` and is never pruned: it is shared
across modules, so anything application-specific stored on it would accumulate
across every project in the database.
Attached at types, callables and fields. Python attaches at classes and callables
only, but TS carries decorators on fields too and property decorators are a
first-class TS idiom (Angular `@Input`, TypeORM `@Column`), so dropping them would
lose the metadata most TS decorator queries are actually after. Parameters are not
projected as nodes in either analyzer, so parameter decorators stay unprojected.
SCHEMA_VERSION stays at 2.1.0 — every analyzer re-baselines together later.
|
Correction to a claim in this description, found while propagating python 1.4.1's entrypoint changes into the TS spec (#150). The description says the merge key is "the checker-resolved Practical effect of this PR as merged: Fix is scoped as unit 0 of the entrypoint spec: |
…or absent (#152) `TSDecorator.qualified_name` was documented as "checker-resolved FQN when available". It was ts-morph's `Decorator.getFullName()` -- the written expression text -- and the builder never consulted the checker. Measured with `import { Get as HttpGet }`, where resolution is trivial: the field emitted `HttpGet`, the alias as typed. Now `qualified_name` is the import-table resolution of the written spelling, or ABSENT when the decorator's head is not an imported binding -- python's rule ("Jedi, else the import table, else None") minus the Jedi tier TypeScript does not have. Aliases map back to the exported name; module specifiers are kept verbatim, which is the spelling a rule names. `name` becomes the spelling as WRITTEN (`http.route`, not `route`). The full spelling used to survive only in `qualified_name`; once that is resolved-or-absent, an unresolved dotted decorator would otherwise lose it from the output entirely. Python's `name` is the written spelling for the same reason. Effect on #143's Neo4j merge (`qualified_name || name`): imported decorators now merge on a package-qualified key, same-file ones on their written spelling. That is what the PR described and the opposite of what it did -- a local `@Get` and NestJS's `@Get` collapsed into one node. The resolver is its own module because the entrypoint pass (#72 units 2-3) needs the same import table for base classes and the unresolved counter.
Closes #82.
Decorators have been captured as structured
TSDecoratorin the JSON since the beginning — name,checker-resolved
qualified_name, positional and keyword arguments, spans — on types, callables,fields and parameters. None of it reached Neo4j. The entire projection mentioned decorators once,
in a comment (
rows.ts:62), so a query could see@Controllerinanalysis.jsonand not in thegraph. Python has projected them since #128.
What this adds
Mirrors python's
_project_decorator(codeanalyzer/neo4j/project.py:645)::TSDecorator, merged on the resolvedqualified_namewhen the checker supplies one, so@Get('/')and@Get('/:id')land on one node instead of two spellings of the samedecorator.
TS_DECORATED_BYfrom the decorated node to it, carrying the per-application facts —positional_arguments(raw source fragments) andkeyword_arguments_json(sorted-key JSON,because Neo4j has no map property type; python encodes the same field the same way, so the two
projections stay diffable).
Like python's, the node carries no
_moduleand is never pruned — it is shared across modules, soanything application-specific on it would accumulate across every project in the database. The
per-application facts live on the relationship for exactly that reason.
One deliberate divergence from python
Python attaches at classes and callables. This attaches at types, callables and fields.
TS carries decorators on fields in the schema already, and property decorators are a first-class TS
idiom — Angular
@Input, TypeORM@Column, NestJS. Dropping them would lose the metadata most TSdecorator queries are actually after. Parameters are not projected as nodes in either analyzer, so
parameter decorators remain unprojected in both.
Verification
bun test— 238 pass, 0 fail (3 new)bun run test:container, 4 pass against live Neo4j, which is whatexercises the auto-derived
tsdecorator_nameuniqueness constraint in real DDLbun run typecheckclean,schema.neo4j.jsonregeneratedThe new test pins the two properties that row counts alone would not catch: the node is shared
across applications, and the arguments are per-application and therefore on the relationship.
Verified on the existing
sample-appfixture, which was already decorated:Note on #82's stated shape
The issue title proposes
decorators: string[]onTSCallable. That would be worse than whatthe analyzer already captures — it flattens structured decorators to bare names, drops the
arguments and the resolved FQN, and covers only callables when the schema carries decorators on
four node types. This implements the python-parity shape instead, which is what "full parity"
requires. Say the word if you want the flattened form as well as, or instead of, this.
SCHEMA_VERSIONstays at 2.1.0 — per the standing decision that only 2.0.0 is meaningful untilthe design settles and every analyzer re-baselines together.