Skip to content

Commit 2fe4d54

Browse files
committed
Reject template property-keys (TS1136) and tighten enum members (TS1357)
Two grammar over-accepts, both from a failed declaration falling back to a looser parse: - A template literal as an object/class property key (`{ `b`: 321 }`) was explicitly allowed by a `[Template, …]` branch in Prop. No valid JS/TS permits a template property key (only ident/string/number/computed/#private), so the branch is removed. A tagged template `obj.tag`s`` is unaffected (it parses as a postfix expression, not a property key). - An enum member followed by `:` (`enum E { a: 1 }`) was accepted because the enum Decl failed on the `:` and fell back to parsing `enum` as an identifier expression + `E` + a `{…}` block. Two fixes: forbid `enum` as a standalone expression identifier (`[not('enum'), Ident]` — enum is reserved), and widen EnumMember's name to MemberName (covers numeric/bigint/#private members) so the enum Decl path remains the only parse and valid numeric-member enums still parse. FP 103->95 (8 cases), FN stays 0 (3376/3376), TP unchanged. Artifacts regenerated.
1 parent f3d5ca1 commit 2fe4d54

4 files changed

Lines changed: 10 additions & 13 deletions

File tree

examples/lezer/typescript.grammar

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Expr {
9898
Expr !l16 (Op_2b2b | Op_2d2d)
9999
}
100100
Prop {
101-
(Spread Expr | (kw_public | kw_private | kw_protected)? (kw_get | kw_set) MemberName ParenL ((Param (Comma Param)*)?)? ParenR (Colon Type)? Block | kw_async? Op_2a? MemberName TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block | Template (Colon Expr | ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block) | MemberName Colon Expr | BracketL Expr (Comma Expr)* BracketR Colon Expr | Ident (Op_3d Expr | Question (Colon Expr)? | ))
101+
(Spread Expr | (kw_public | kw_private | kw_protected)? (kw_get | kw_set) MemberName ParenL ((Param (Comma Param)*)?)? ParenR (Colon Type)? Block | kw_async? Op_2a? MemberName TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block | MemberName Colon Expr | BracketL Expr (Comma Expr)* BracketR Colon Expr | Ident (Op_3d Expr | Question (Colon Expr)? | ))
102102
}
103103
MemberName {
104104
(Ident | PrivateField | String | Number | HexNumber | OctalNumber | BinaryNumber | BigInt | BracketL Expr BracketR)
@@ -158,7 +158,7 @@ ClassMember {
158158
(DecoratorExpr | kw_constructor ParenL (Param (Comma Param)*)? ParenR Block Semi? | kw_static Block | (kw_public | kw_private | kw_protected | kw_static | kw_abstract | kw_readonly | kw_override | kw_accessor | kw_async)* (Op_2a MemberName Question? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block? Semi? | (kw_get | kw_set) MemberName ParenL ((Param (Comma Param)*)?)? ParenR (Colon Type)? Block? Semi? | BracketL Ident Colon Type BracketR Colon Type Semi? | MemberName (Question? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block? Semi? | Op_21? Question? (Colon Type)? (Op_3d Expr)? Semi?)) | MemberName Op_21? Question? (Colon Type)? (Op_3d Expr)? Semi? | MemberName Question? TypeParams? ParenL (Param (Comma Param)*)? ParenR (Colon Type)? Block? Semi?)
159159
}
160160
EnumMember {
161-
(Ident | String | BracketL Expr BracketR) (Op_3d Expr)?
161+
MemberName (Op_3d Expr)?
162162
}
163163
ImportClause {
164164
(Ident (Comma (BraceL (ImportSpecifier (Comma ImportSpecifier)*)? BraceR | Op_2a kw_as Ident))? | BraceL (ImportSpecifier (Comma ImportSpecifier)*)? BraceR | Op_2a kw_as Ident)

examples/tree-sitter/grammar.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ module.exports = grammar({
105105
seq("<", $.type, ">", $.expr)
106106
),
107107

108-
prop: $ => choice(seq("...", $.expr), seq(optional(choice("public", "private", "protected")), choice("get", "set"), $.member_name, "(", optional(optional(seq($.param, repeat(seq(",", $.param)), optional(",")))), ")", optional(seq(":", $.type)), $.block), seq(optional("async"), optional("*"), $.member_name, optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), $.block), seq($.template, choice(seq(":", $.expr), seq("(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), $.block))), seq($.member_name, ":", $.expr), seq("[", $.expr, repeat(seq(",", $.expr)), "]", ":", $.expr), seq($.ident, choice(seq("=", $.expr), seq("?", optional(seq(":", $.expr))), blank()))),
108+
prop: $ => choice(seq("...", $.expr), seq(optional(choice("public", "private", "protected")), choice("get", "set"), $.member_name, "(", optional(optional(seq($.param, repeat(seq(",", $.param)), optional(",")))), ")", optional(seq(":", $.type)), $.block), seq(optional("async"), optional("*"), $.member_name, optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), $.block), seq($.member_name, ":", $.expr), seq("[", $.expr, repeat(seq(",", $.expr)), "]", ":", $.expr), seq($.ident, choice(seq("=", $.expr), seq("?", optional(seq(":", $.expr))), blank()))),
109109

110110
member_name: $ => choice($.ident, $.private_field, $.string, $.number, $.hex_number, $.octal_number, $.binary_number, $.big_int, seq("[", $.expr, "]")),
111111

@@ -145,7 +145,7 @@ module.exports = grammar({
145145

146146
class_member: $ => choice($.decorator_expr, seq("constructor", "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", $.block, optional(";")), seq("static", $.block), seq(repeat(choice("public", "private", "protected", "static", "abstract", "readonly", "override", "accessor", "async")), choice(seq("*", $.member_name, optional("?"), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), optional($.block), optional(";")), seq(choice("get", "set"), $.member_name, "(", optional(optional(seq($.param, repeat(seq(",", $.param)), optional(",")))), ")", optional(seq(":", $.type)), optional($.block), optional(";")), seq("[", $.ident, ":", $.type, "]", ":", $.type, optional(";")), seq($.member_name, choice(seq(optional("?"), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), optional($.block), optional(";")), seq(optional("!"), optional("?"), optional(seq(":", $.type)), optional(seq("=", $.expr)), optional(";")))))), seq($.member_name, optional("!"), optional("?"), optional(seq(":", $.type)), optional(seq("=", $.expr)), optional(";")), seq($.member_name, optional("?"), optional($.type_params), "(", optional(seq($.param, repeat(seq(",", $.param)), optional(","))), ")", optional(seq(":", $.type)), optional($.block), optional(";"))),
147147

148-
enum_member: $ => seq(choice($.ident, $.string, seq("[", $.expr, "]")), optional(seq("=", $.expr))),
148+
enum_member: $ => seq($.member_name, optional(seq("=", $.expr))),
149149

150150
import_clause: $ => choice(seq($.ident, optional(seq(",", choice(seq("{", optional(seq($.import_specifier, repeat(seq(",", $.import_specifier)), optional(","))), "}"), seq("*", "as", $.ident))))), seq("{", optional(seq($.import_specifier, repeat(seq(",", $.import_specifier)), optional(","))), "}"), seq("*", "as", $.ident)),
151151

examples/typescript.cst-types.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,9 @@ export interface PropNode extends CstPos {
155155
kind: 'node';
156156
rule: 'Prop';
157157
children: Array<
158-
| $templateNode
159158
| (CstLeaf & { tokenType: '$keyword' })
160159
| (CstLeaf & { tokenType: '$punct' })
161-
| (CstLeaf & { tokenType: '$templateHead' })
162160
| (CstLeaf & { tokenType: 'Ident' })
163-
| (CstLeaf & { tokenType: 'Template' })
164161
| BlockNode
165162
| ExprNode
166163
| MemberNameNode
@@ -445,9 +442,8 @@ export interface EnumMemberNode extends CstPos {
445442
rule: 'EnumMember';
446443
children: Array<
447444
| (CstLeaf & { tokenType: '$punct' })
448-
| (CstLeaf & { tokenType: 'Ident' })
449-
| (CstLeaf & { tokenType: 'String' })
450445
| ExprNode
446+
| MemberNameNode
451447
>;
452448
}
453449

examples/typescript.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,6 @@ const Prop = rule($ => {
152152
[opt(alt('public', 'private', 'protected')), alt('get', 'set'), MemberName, '(', opt(sep(Param, ',')), ')', opt(':', Type), Block],
153153
// method: async?/generator?, any member name (incl `#x`, computed `[e]`), then ( … ) { … }
154154
[opt('async'), opt('*'), MemberName, opt(TypeParams), ...method],
155-
// template-literal key: value | method
156-
[Template, alt([':', Expr], method)],
157155
// value property — any member name incl computed `[e]: v` (MemberName covers `[Expr]`)
158156
[MemberName, ':', Expr],
159157
['[', Expr, many(',', Expr), ']', ':', Expr], // computed comma list (lenient)
@@ -177,7 +175,10 @@ const NewTarget = rule($ => [
177175
]);
178176

179177
const Expr = rule($ => [
180-
Ident,
178+
// `enum` is reserved — it can't be a standalone expression identifier. Without this
179+
// an invalid `enum E { a: 1 }` (where the enum Decl fails on `:`) would fall back to
180+
// parsing `enum` as an identifier expr + `E` + `{…}` block, wrongly accepting it.
181+
[not('enum'), Ident],
181182
Number_,
182183
String_,
183184
Template,
@@ -416,7 +417,7 @@ const ClassMember = rule($ => [
416417
]);
417418

418419
const EnumMember = rule($ => [
419-
[alt(Ident, String_, ['[', Expr, ']']), opt('=', Expr)],
420+
[MemberName, opt('=', Expr)],
420421
]);
421422

422423
const ImportSpecifier = rule($ => [

0 commit comments

Comments
 (0)