Skip to content

Commit e632ff8

Browse files
committed
Reject property access on a bare instantiation expression (TS1477)
`Foo<T>.Bar()` — a bare type-argument instantiation followed by property access — is a TS error (TS1477), but our access LEDs chained `.Bar` straight onto the instantiation. Same family as the prior 'postfix closes the access tail' fix. Grammar: split Expr's instantiation LED into a call/tag arm (an ordinary access tail — `f<T>().x` still chains) and a separate bare-instantiation arm ending in the negative lookahead `not(Expr)`. Engine (still language-agnostic): a LED whose last item is a zero-width negative lookahead is a 'tail-closing' LED — once it binds, it sets tailClosed, so the access-tail LEDs (`.`/`?.`/`[`/call/`<T>`/tagged-template) can't chain off it. Keyed purely on the structural shape '`led` ends in a not assertion', no TS knowledge; there is exactly one such LED in the grammar. Verified: rejects `Foo<T>.Bar()`/`Foo<T>.Bar<T>()`; still parses `Foo<T>()`, `Foo.Bar<T>()`, `Foo<T>`x`` (tagged), `Foo<T>().x`, `Foo<T> as Bar`, `Foo<T>[0]`. FP 95->93, FN stays 0 (3376/3376), TP unchanged. agnostic 5/5, refactor-guard 112/112, highlighter 99.3% unchanged, smoke pass. Artifacts regenerated.
1 parent 2fe4d54 commit e632ff8

4 files changed

Lines changed: 29 additions & 7 deletions

File tree

examples/lezer/typescript.grammar

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ Expr {
4848
kw_this |
4949
kw_super |
5050
Spread Expr |
51-
Expr AngleL (Type (Comma Type)*)? AngleR (ParenL (Expr (Comma Expr)*)? ParenR | Template | ) |
51+
Expr AngleL (Type (Comma Type)*)? AngleR (ParenL (Expr (Comma Expr)*)? ParenR | Template) |
52+
Expr AngleL (Type (Comma Type)*)? AngleR |
5253
Expr ParenL (Expr (Comma Expr)*)? ParenR |
5354
Expr Dot (Ident | PrivateField) |
5455
Expr QuestionDot (Ident | PrivateField | ParenL (Expr (Comma Expr)*)? ParenR | BracketL Expr BracketR | Template) |

examples/tree-sitter/grammar.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ module.exports = grammar({
7272
"this",
7373
"super",
7474
seq("...", $.expr),
75-
prec.left(18, seq($.expr, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">", choice(seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), $.template, blank()))),
75+
prec.left(18, seq($.expr, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">", choice(seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), $.template))),
76+
prec.left(18, seq($.expr, "<", optional(seq($.type, repeat(seq(",", $.type)), optional(","))), ">")),
7677
prec.left(18, seq($.expr, "(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")")),
7778
prec.left(18, seq($.expr, ".", choice($.ident, $.private_field))),
7879
prec.left(18, seq($.expr, "?.", choice($.ident, $.private_field, seq("(", optional(seq($.expr, repeat(seq(",", $.expr)), optional(","))), ")"), seq("[", $.expr, "]"), $.template))),

examples/typescript.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,18 @@ const Expr = rule($ => [
188188
[prefix, $],
189189
[$, postfix],
190190
['...', $],
191-
// instantiation / typed call / tagged template: f<T> | f<T>(…) | f<T>`…`
192-
// A bare instantiation `f<T>` (no call/tag) only when the next token can't
193-
// start an expression — otherwise `<`/`>` were comparisons (`f < a, b > 7`):
194-
// the same disambiguation TS makes via canFollowTypeArgumentsInExpression.
195-
[$, '<', sep(Type, ','), '>', alt(['(', sep($, ','), ')'], Template, not(Expr))],
191+
// typed call / tagged template: f<T>(…) | f<T>`…` — a call/tag may itself be
192+
// continued by member access (`f<T>().x`), so this is an ordinary access tail.
193+
[$, '<', sep(Type, ','), '>', alt(['(', sep($, ','), ')'], Template)],
194+
// bare instantiation `f<T>` (no call/tag): allowed only when the next token
195+
// can't start an expression — otherwise `<`/`>` were comparisons (`f < a, b > 7`),
196+
// the disambiguation TS makes via canFollowTypeArgumentsInExpression. Ending in a
197+
// negative lookahead, this LED closes the access tail (it asserts nothing follows),
198+
// so a `.`/`?.` property access can't chain off it: `Foo<T>.Bar` is rejected
199+
// (TS1477 — a bare instantiation is not a valid base for property access). A `[`,
200+
// `(`, or `` ` `` continuation still reparses the `<…>` as comparisons (those start
201+
// an expression, so `not($)` fails the bare arm), matching TS.
202+
[$, '<', sep(Type, ','), '>', not(Expr)],
196203
[$, '(', sep($, ','), ')'],
197204
[$, '.', alt(Ident, PrivateField)],
198205
// optional chaining: ?.x | ?.#x | ?.(args) | ?.[i] | ?.`…`

src/gen-parser.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ export function createParser(grammar: CstGrammar) {
240240
// binary/ternary `… in $` / `? $ : $`) AND whose connector is a punctuator, not a
241241
// word-operator — so `as`/`satisfies`/`in`/`instanceof`/`?:` still bind after `a++`.
242242
const accessTailLeds = new Set<object>();
243+
// ── Tail-closing LEDs (a LED that itself closes the access tail) ──
244+
// A LED ending in a zero-width *negative lookahead* (`… not($)`) asserts that
245+
// nothing may follow it, so its result cannot be the base of any further
246+
// member/element/call access — once it binds, the access tail is closed (no `.x`,
247+
// `[i]`, `(…)`, `<T>`, tagged template). Language-agnostic / structural: keyed on
248+
// the LED ending in a `not` assertion, with no knowledge of what it guards. (E.g.
249+
// a bare type-argument instantiation `Foo<T>` — written `… '>' not($)` — which TS
250+
// forbids from being followed by property access, TS1477.)
251+
const tailClosingLeds = new Set<object>();
243252
for (const [ruleName, { leds }] of prattClassified.entries()) {
244253
for (const led of leds) {
245254
const it = led.items;
@@ -249,6 +258,7 @@ export function createParser(grammar: CstGrammar) {
249258
const lastIsOperand = selfRefName(last, ruleName); // open binary/ternary operand
250259
const wordConnector = it[0].type === 'literal' && /^[A-Za-z]/.test(it[0].value);
251260
if (!lastIsOperand && !wordConnector) accessTailLeds.add(led);
261+
if (last.type === 'not') tailClosingLeds.add(led);
252262
}
253263
}
254264

@@ -673,6 +683,9 @@ export function createParser(grammar: CstGrammar) {
673683
offset: lhs.offset,
674684
end: children.length > 0 ? childEnd(children[children.length - 1]) : lhs.end,
675685
};
686+
// A LED ending in a negative lookahead (e.g. a bare instantiation
687+
// `Foo<T>`) closes the access tail: nothing may chain off it.
688+
if (tailClosingLeds.has(led)) tailClosed = true;
676689
matched = true;
677690
break;
678691
}

0 commit comments

Comments
 (0)