Skip to content

feat(neo4j): project decorators, matching python - #143

Merged
rahlk merged 1 commit into
mainfrom
feat/issue-82-neo4j-decorators
Sep 3, 2026
Merged

feat(neo4j): project decorators, matching python#143
rahlk merged 1 commit into
mainfrom
feat/issue-82-neo4j-decorators

Conversation

@rahlk

@rahlk rahlk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Closes #82.

Decorators have been captured as structured TSDecorator in 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 @Controller in analysis.json and not in the
graph. Python has projected them since #128.

What this adds

Mirrors python's _project_decorator (codeanalyzer/neo4j/project.py:645):

  • :TSDecorator, merged on the resolved qualified_name when the checker supplies one, so
    @Get('/') and @Get('/:id') land on one node instead of two spellings of the same
    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,
    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 _module and is never pruned — it is shared across modules, so
anything 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 TS
decorator 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)
  • container tests runbun run test:container, 4 pass against live Neo4j, which is what
    exercises the auto-derived tsdecorator_name uniqueness constraint in real DDL
  • bun run typecheck clean, schema.neo4j.json regenerated

The 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-app fixture, which was already decorated:

{k: 'Controller', p: {name: 'Controller', qualified_name: 'Controller'}},
{k: 'Get',        p: {name: 'Get',        qualified_name: 'Get'}}

{f: '…/UserController/list', t: 'Get',        p: {positional_arguments: ['"/"'],      keyword_arguments_json: '{}'}},
{f: '…/UserController/show', t: 'Get',        p: {positional_arguments: ['"/:id"'],   keyword_arguments_json: '{}'}},
{f: '…/UserController',      t: 'Controller', p: {positional_arguments: ['"/users"'], keyword_arguments_json: '{}'}}

Note on #82's stated shape

The issue title proposes decorators: string[] on TSCallable. That would be worse than what
the 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_VERSION stays at 2.1.0 — per the standing decision that only 2.0.0 is meaningful until
the design settles and every analyzer re-baselines together.

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.
@rahlk
rahlk merged commit c316f71 into main Sep 3, 2026
1 check passed
@rahlk
rahlk deleted the feat/issue-82-neo4j-decorators branch September 3, 2026 02:46
@rahlk

rahlk commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

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 qualified_name when the checker supplies one". It never does. qualified_name is Decorator.getFullName() — the written expression text — and the builder does not consult the checker (builders.ts:178). Measured with a decorator imported as import { Get as HttpGet } from "./decorators", where resolution is trivial: the field is HttpGet, the alias as typed.

Practical effect of this PR as merged: :TSDecorator merges on the written spelling, so a project's local @Get and NestJS's @Get collapse into one node. The collapse-two-spellings-into-one behaviour this PR describes therefore works in the opposite direction from python's — it merges things that are different, not spellings of the same thing.

Fix is scoped as unit 0 of the entrypoint spec: qualified_name becomes the import-table resolution or absent, python's exact rule. It ships as its own PR because it changes emitted values.

rahlk added a commit that referenced this pull request Sep 6, 2026
…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.
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.

v2 Neo4j build: project decorators (decorators: string[] on TSCallable)

1 participant