Skip to content

Commit ba158c0

Browse files
committed
emit-portable: no-in suppress, +-quantifier, sep/bracket fixes — javascript.ts now EMITS
A run of constructs that together take the real javascript.ts grammar through the whole portable emitter end-to-end: - no-`in` (suppress) context: a `for (binding in iterable)` head parses its binding with the `in` led disabled (examples/noinjs.ts, 9/9+4/4 ×3). Threads a suppressed-connector set consumed per led loop. - one-or-more `+` quantifier (`x+` = `x x*`) — the last buildIR throw; with it, javascript.ts EMITS in all three targets. - Two latent `sep` bugs, both exposed only by the real grammar (earlier grammars wrapped sep in opt or never tested the shapes — the aggregate passed for the wrong reason): gen-parser's sep is `(element (delim element)*)?`, i.e. the WHOLE list is optional (empty `f()` valid) AND a trailing delimiter is allowed. sepBy now matches. - A NUD bracket that fails now FALLS THROUGH to the next same-first-token alternative instead of returning null — javascript has four `new`-led NUDs. Result: javascript.ts emits, compiles and runs in ts/go/rust, and is byte-identical to createParser on basic JS (var/function/arrow/ternary/member-call/for-in/while/if/class/ new/template/regex/instanceof/try/switch) — 23/24 in TS, the one miss a `new a.b()` NewTarget member-constructor CST shape. The await/yield fork (async/await) and that new-expression edge remain. Full suite 42/42; existing gate unaffected by the shared sep/bracket fixes.
1 parent 544e277 commit ba158c0

6 files changed

Lines changed: 78 additions & 23 deletions

File tree

examples/noinjs.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Exercises the no-`in` (suppress) context. In a `for (binding in iterable)` head, the
2+
// binding is parsed with the `in` LED DISABLED — `exclude('in', Expr)` — so the `in` belongs
3+
// to the for-head, not to a relational expression inside the binding. Outside a for-head, `in`
4+
// binds normally. The portable parser threads a suppressed-connector set into the led loop.
5+
import {
6+
token, rule, defineGrammar, left, op, exclude,
7+
seq, oneOf, range, star, many,
8+
} from '../src/api.ts';
9+
10+
const idStart = oneOf(range('a', 'z'), range('A', 'Z'), '_', '$');
11+
const idCont = oneOf(range('a', 'z'), range('A', 'Z'), range('0', '9'), '_', '$');
12+
const Ident = token(seq(idStart, star(idCont)), { identifier: true, scope: 'variable' });
13+
const Number_ = token(seq(range('0', '9'), star(range('0', '9'))), { scope: 'constant.numeric' });
14+
15+
const jsPrec = [left('||'), left('<', '>'), left('+', '-')];
16+
17+
const Expr = rule(($) => [
18+
Number_, Ident,
19+
['(', $, ')'],
20+
[$, op, $],
21+
[$, 'in', $],
22+
[$, '.', Ident],
23+
]);
24+
const ForHead = rule(($) => [['for', '(', exclude('in', Expr), 'in', Expr, ')', Stmt]]);
25+
const Stmt = rule(($) => [ForHead, [Expr, ';']]);
26+
const Program = rule(($) => [many(Stmt)]);
27+
28+
export default defineGrammar({
29+
name: 'noinjs',
30+
scopeName: 'source.noinjs',
31+
tokens: { Ident, Number: Number_ },
32+
prec: jsPrec,
33+
ledPrec: [{ connector: 'in', sameAs: '<', chainRhs: true }],
34+
rules: { Expr, ForHead, Stmt, Program },
35+
});

src/emit-portable.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export type Step =
5454
| { t: 'alt'; branches: Step[][] } // inline alternation of sub-sequences (backtracking)
5555
| { t: 'not'; steps: Step[] } // zero-width negative lookahead (consumes nothing)
5656
| { t: 'seq'; steps: Step[] } // a grouped sub-sequence (e.g. a star body `(',' Expr)`)
57-
| { t: 'sameLine' }; // zero-width: the next token is on the same line (no preceding newline)
57+
| { t: 'sameLine' } // zero-width: the next token is on the same line (no preceding newline)
58+
| { t: 'suppress'; connectors: string[]; steps: Step[] }; // parse the body with these LED connectors disabled (no-`in` context)
5859
export type Alt = Step[];
5960

6061
export type RdRule = { kind: 'rd'; name: string; alts: Alt[] };
@@ -144,9 +145,9 @@ function buildIR(grammar: CstGrammar): ParserIR {
144145
switch (e.type) {
145146
case 'literal': return { t: 'lit', value: e.value, ttype: litTtype(e.value) };
146147
case 'ref': return tokenNames.has(e.name) ? { t: 'tok', name: e.name } : { t: 'rule', name: e.name };
147-
case 'group': { // transparent (ctxMode is invisible to the portable parser); only no-in `suppress` is deferred
148-
if (e.suppress) throw new Error('portable: group with suppress (no-in context) not yet in scope');
148+
case 'group': { // transparent (ctxMode is invisible to the portable parser)
149149
const ss = altSteps(e.body);
150+
if (e.suppress && e.suppress.length) return { t: 'suppress', connectors: e.suppress, steps: ss }; // no-`in` context
150151
return ss.length === 1 ? ss[0] : { t: 'seq', steps: ss };
151152
}
152153
case 'not': return { t: 'not', steps: altSteps(e.body) }; // zero-width negative lookahead
@@ -156,7 +157,7 @@ function buildIR(grammar: CstGrammar): ParserIR {
156157
case 'quantifier':
157158
if (e.kind === '*') return { t: 'star', step: stepOf(e.body) };
158159
if (e.kind === '?') return { t: 'opt', steps: altSteps(e.body) };
159-
if (e.kind === '+') throw new Error("portable: '+' not yet modeled (use '*')");
160+
if (e.kind === '+') return { t: 'seq', steps: [stepOf(e.body), { t: 'star', step: stepOf(e.body) }] }; // x+ = x x*
160161
break;
161162
case 'alt': {
162163
if (e.items.every((it) => it.type === 'literal')) { // fast path: all-literal alternation
@@ -289,15 +290,11 @@ function buildPratt(
289290
continue;
290291
}
291292
if (items[0].type === 'literal') { nudBrackets.push({ first: items[0].value, steps: items.map((it) => stepOfPratt(it)) }); continue; }
292-
// A single transparent group unwraps to its body (an explicit grouping of the NUD sequence).
293+
// A single transparent (non-suppress) group unwraps to its body (an explicit grouping).
293294
let nudItems = items;
294295
if (items.length === 1 && items[0].type === 'group' && !items[0].suppress) {
295296
nudItems = items[0].body.type === 'seq' ? items[0].body.items : [items[0].body];
296297
}
297-
// A no-`in`/suppress group is a deeper construct — defer.
298-
if (nudItems.some((it) => it.type === 'group' && it.suppress)) {
299-
throw new Error(`portable: Pratt NUD with suppress (no-in context) not yet in scope (rule ${name})`);
300-
}
301298
nudSeqs.push(nudItems.map((it) => stepOfPratt(it))); // general NUD sequence (guarded ident, class expr)
302299
continue;
303300
}
@@ -328,13 +325,15 @@ function buildPratt(
328325
if (e.type === 'seq') return { t: 'seq', steps: e.items.map(stepOfPratt) };
329326
if (e.type === 'sameLine') return { t: 'sameLine' };
330327
if (e.type === 'not') return { t: 'not', steps: (e.body.type === 'seq' ? e.body.items : [e.body]).map(stepOfPratt) };
328+
if (e.type === 'group' && e.suppress && e.suppress.length) return { t: 'suppress', connectors: e.suppress, steps: (e.body.type === 'seq' ? e.body.items : [e.body]).map(stepOfPratt) };
331329
// ctxMode (await/yield) is transparent to the portable parser (no fork); unwrap the group.
332-
if (e.type === 'group' && !e.capBelow && !e.suppress) {
330+
if (e.type === 'group' && !e.capBelow) {
333331
return e.body.type === 'seq' ? { t: 'seq', steps: e.body.items.map(stepOfPratt) } : stepOfPratt(e.body);
334332
}
335333
if (e.type === 'sep') return { t: 'sep', elem: stepOfPratt(e.element), delim: e.delimiter };
336334
if (e.type === 'quantifier' && e.kind === '?') return { t: 'opt', steps: (e.body.type === 'seq' ? e.body.items : [e.body]).map(stepOfPratt) };
337335
if (e.type === 'quantifier' && e.kind === '*') return { t: 'star', step: stepOfPratt(e.body) };
336+
if (e.type === 'quantifier' && e.kind === '+') return { t: 'seq', steps: [stepOfPratt(e.body), { t: 'star', step: stepOfPratt(e.body) }] };
338337
if (e.type === 'literal') return { t: 'lit', value: e.value, ttype: litTtype(e.value) };
339338
return stepOf(e);
340339
}

src/target-go.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function stepCond(s: Step): string {
178178
case 'not': return `func() bool { save := pos; sb := len(scratch); nb := len(nodes); kb := len(kids); m := ${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'}; pos = save; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]; return !m }()`;
179179
case 'seq': return `(${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'})`;
180180
case 'sameLine': return `func() bool { t := peek(); return t != nil && !t.Nl }()`;
181+
case 'suppress': return `func() bool { _suppressNext = map[string]bool{${s.connectors.map((c) => `${J(c)}: true`).join(', ')}}; _r := (${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'}); _suppressNext = nil; return _r }()`;
181182
}
182183
}
183184

@@ -207,9 +208,9 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
207208
const bracketNud = (b: Bracket) => `\tif t.Text == ${J(b.first)} {
208209
\t\tsave := pos; sb := len(scratch); nb := len(nodes); kb := len(kids)
209210
\t\tif ${b.steps.map(stepCond).join(' && ')} { return finish(${J(r.name)}, sb, t.Off) }
210-
\t\tpos = save; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]; return -1
211+
\t\tpos = save; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]
211212
\t}`;
212-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}t.Text == ${J(b.first)} {
213+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => `\t\tif ${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}!_mySup[${J(b.first)}] && t.Text == ${J(b.first)} {
213214
\t\t\tledSave := pos; sb := len(scratch); nb := len(nodes); kb := len(kids)
214215
\t\t\tscratch = append(scratch, left)
215216
\t\t\tif ${b.steps.map(stepCond).join(' && ')} { left = finish(${J(r.name)}, sb, nodes[left].Offset); continue }
@@ -233,6 +234,7 @@ var ${r.name}POST = map[string]int{${post}}
233234
var ${r.name}ATOM = map[string]bool{${atoms}}
234235
func parse${r.name}() int32 { return ${r.name}bp(0) }
235236
func ${r.name}bp(minBp int) int32 {
237+
\t_mySup := _suppressNext; _suppressNext = nil; _ = _mySup
236238
\tleft := ${r.name}nud(minBp)
237239
\tif left < 0 { return -1 }
238240
\tif _capped { return left }
@@ -332,6 +334,7 @@ type bp struct{ lbp, rbp int }
332334
var toks []Tok
333335
var pos int
334336
var _capped bool
337+
var _suppressNext map[string]bool
335338
var nodes []Node
336339
var kids []int32
337340
var scratch []int32
@@ -379,7 +382,7 @@ func opt(body func() bool) bool {
379382
\tsp := pos; sb := len(scratch); nb := len(nodes); kb := len(kids); if !body() { pos = sp; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb] }; return true
380383
}
381384
func sepBy(elem func() bool, delim string) bool {
382-
\tif !elem() { return false }
385+
\tif !elem() { return true } // the whole separated list is optional — zero elements is valid
383386
\tfor {
384387
\t\tsp := pos; sb := len(scratch); nb := len(nodes); kb := len(kids)
385388
\t\tif !matchLit(delim, "$punct") { pos = sp; scratch = scratch[:sb]; nodes = nodes[:nb]; kids = kids[:kb]; break }

src/target-rust.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ function stepCond(s: Step): string {
186186
case 'not': return `(|p: &mut Parser<'a>, k: &mut Vec<Cst>| -> bool { ${notBody(s.steps)} })(self, &mut kids)`;
187187
case 'seq': return `(${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'})`;
188188
case 'sameLine': return `matches!(self.peek(), Some(t) if !t.nl)`;
189+
case 'suppress': return `{ self.suppress_next = vec![${s.connectors.map(J).join(', ')}]; let _r = (${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'}); self.suppress_next = Vec::new(); _r }`;
189190
}
190191
}
191192
// A backtracking inline alternation rendered as an immediately-applied closure over (p, k),
@@ -212,6 +213,7 @@ function stepCondP(s: Step): string {
212213
case 'not': return `(|p: &mut Parser<'a>, k: &mut Vec<Cst>| -> bool { ${notBody(s.steps)} })(p, k)`;
213214
case 'seq': return `(${s.steps.length ? s.steps.map(stepCondP).join(' && ') : 'true'})`;
214215
case 'sameLine': return `matches!(p.peek(), Some(t) if !t.nl)`;
216+
case 'suppress': return `{ p.suppress_next = vec![${s.connectors.map(J).join(', ')}]; let _r = (${s.steps.length ? s.steps.map(stepCondP).join(' && ') : 'true'}); p.suppress_next = Vec::new(); _r }`;
215217
}
216218
}
217219

@@ -237,9 +239,9 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
237239
const bracketNud = (b: Bracket) => ` if t.text == ${J(b.first)} {
238240
let save = self.pos; let mut kids: Vec<Cst> = Vec::new();
239241
if ${b.steps.map(stepCond).join(' && ')} { return Some(node(${J(r.name)}, kids)); }
240-
self.pos = save; return None;
242+
self.pos = save; // fall through to the next NUD alternative
241243
}`;
242-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if ${accessTail ? '!tail_closed && ' : ''}${lbp !== null ? `${lbp} > min_bp && ` : ''}t.text == ${J(b.first)} {
244+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if ${accessTail ? '!tail_closed && ' : ''}${lbp !== null ? `${lbp} > min_bp && ` : ''}!my_sup.iter().any(|c| *c == ${J(b.first)}) && t.text == ${J(b.first)} {
243245
let led_save = self.pos; let mut kids: Vec<Cst> = Vec::new();
244246
if ${b.steps.map(stepCond).join(' && ')} {
245247
let mut full = vec![left]; full.append(&mut kids);
@@ -259,6 +261,8 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
259261
fn ${r.name}_post(op: &str) -> Option<i64> { match op { ${postArms}${postArms ? ', ' : ''}_ => None } }
260262
fn ${r.name}_atom(kind: &str) -> bool { matches!(kind, ${atomArm || '""'}) }
261263
fn ${r.name}_bp(&mut self, min_bp: i64) -> Option<Cst> {
264+
let my_sup = std::mem::take(&mut self.suppress_next);
265+
let _ = &my_sup;
262266
let mut left = self.${r.name}_nud(min_bp)?;
263267
if self.capped { return Some(left); }
264268
let mut tail_closed = false;
@@ -342,7 +346,7 @@ fn node(rule: &'static str, kids: Vec<Cst>) -> Cst { let o = kids[0].offset; let
342346
343347
${lexer(ir)}
344348
345-
struct Parser<'a> { toks: Vec<Tok<'a>>, pos: usize, capped: bool }
349+
struct Parser<'a> { toks: Vec<Tok<'a>>, pos: usize, capped: bool, suppress_next: Vec<&'static str> }
346350
impl<'a> Parser<'a> {
347351
fn peek(&self) -> Option<Tok<'a>> { if self.pos < self.toks.len() { Some(self.toks[self.pos]) } else { None } }
348352
fn branch(&self, rule: &'static str, kids: Vec<Cst>, save: usize) -> Cst {
@@ -367,7 +371,7 @@ impl<'a> Parser<'a> {
367371
let sp = self.pos; let before = kids.len(); if !body(self, kids) { self.pos = sp; kids.truncate(before); } true
368372
}
369373
fn sep_by(&mut self, elem: fn(&mut Parser<'a>, &mut Vec<Cst>) -> bool, delim: &str, kids: &mut Vec<Cst>) -> bool {
370-
if !elem(self, kids) { return false; }
374+
if !elem(self, kids) { return true; } // the whole separated list is optional — zero elements is valid
371375
loop {
372376
let sp = self.pos; let before = kids.len();
373377
if !self.match_lit(delim, "$punct", kids) { self.pos = sp; kids.truncate(before); break; }
@@ -399,15 +403,15 @@ fn main() {
399403
// Self-bench: a numeric arg N times the lex+parse loop and prints ms/iteration.
400404
if let Some(iters) = std::env::args().nth(1).and_then(|a| a.parse::<u64>().ok()) {
401405
// black_box on the input + result so the optimizer can't elide the lex/parse.
402-
for _ in 0..3 { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false }; std::hint::black_box(p.parse_${ir.entry}()); }
406+
for _ in 0..3 { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new() }; std::hint::black_box(p.parse_${ir.entry}()); }
403407
let t = std::time::Instant::now();
404-
for _ in 0..iters { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false }; std::hint::black_box(p.parse_${ir.entry}()); }
408+
for _ in 0..iters { let toks = lex(std::hint::black_box(&src)); let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new() }; std::hint::black_box(p.parse_${ir.entry}()); }
405409
println!("{:.4}", t.elapsed().as_secs_f64() * 1000.0 / iters as f64);
406410
return;
407411
}
408412
let toks = lex(&src);
409413
let n = toks.len();
410-
let mut p = Parser { toks, pos: 0, capped: false };
414+
let mut p = Parser { toks, pos: 0, capped: false, suppress_next: Vec::new() };
411415
match p.parse_${ir.entry}() {
412416
Some(root) if p.pos == n => { let mut out = String::new(); write_json(&root, &mut out); print!("{}", out); }
413417
_ => { eprintln!("parse error (pos {}/{})", p.pos, n); std::process::exit(1); }

src/target-ts.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ function stepCond(s: Step): string {
169169
case 'not': return `(() => { const sp = pos; const bk = kids.length; const m = ${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'}; pos = sp; kids.length = bk; return !m; })()`;
170170
case 'seq': return `(${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'})`;
171171
case 'sameLine': return `(() => { const t = peek(); return t !== null && !t.nl; })()`;
172+
case 'suppress': return `(() => { _suppressNext = new Set([${s.connectors.map(J).join(', ')}]); const _r = (${s.steps.length ? s.steps.map(stepCond).join(' && ') : 'true'}); _suppressNext = null; return _r; })()`;
172173
}
173174
}
174175

@@ -192,11 +193,11 @@ function prattRule(r: PrattRule, tpl: TplCfg | null): string {
192193
const bracketNud = (b: Bracket) => ` if (t.text === ${J(b.first)}) {
193194
const save = pos; const kids: Cst[] = [];
194195
if (${b.steps.map(stepCond).join(' && ')}) return node(${J(r.name)}, kids);
195-
pos = save; return null;
196+
pos = save; // fall through to the next NUD alternative (e.g. another '${b.first}'-led form)
196197
}`;
197198
// Access-tail leds (member/call/index) are disabled once a postfix has closed the operand;
198199
// a precedence-gated led (ternary/in/instanceof) binds only when its lbp > minBp.
199-
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if (${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}t.text === ${J(b.first)}) {
200+
const ledArm = (b: Bracket, accessTail: boolean, lbp: number | null) => ` if (${accessTail ? '!tailClosed && ' : ''}${lbp !== null ? `${lbp} > minBp && ` : ''}(_mySup === null || !_mySup.has(${J(b.first)})) && t.text === ${J(b.first)}) {
200201
const ledSave = pos; const kids: Cst[] = [left];
201202
if (${b.steps.map(stepCond).join(' && ')}) { left = node(${J(r.name)}, kids); continue; }
202203
pos = ledSave; break;
@@ -214,6 +215,7 @@ const ${r.name}_POST: Record<string, number> = ${POST};
214215
const ${r.name}_ATOM = ${atom};
215216
function parse${r.name}(): Node | null { return ${r.name}_bp(0); }
216217
function ${r.name}_bp(minBp: number): Node | null {
218+
const _mySup = _suppressNext; _suppressNext = null; // no-in: consume the suppressed-connector set for this led loop
217219
let left = ${r.name}_nud(minBp);
218220
if (left === null) return null;
219221
if (_capped) return left; // an assignment-level arrow admits no led
@@ -293,6 +295,7 @@ ${lexer(ir)}
293295
let toks: Tok[] = [];
294296
let pos = 0;
295297
let _capped = false;
298+
let _suppressNext: Set<string> | null = null;
296299
function peek(): Tok | null { return pos < toks.length ? toks[pos] : null; }
297300
function branch(rule: string, kids: Cst[], save: number): Node {
298301
const offset = kids.length > 0 ? kids[0].offset : (save < toks.length ? toks[save].off : 0);
@@ -325,7 +328,7 @@ function opt(body: () => boolean, kids: Cst[]): boolean {
325328
const sp = pos; const before = kids.length; if (!body()) { pos = sp; kids.length = before; } return true;
326329
}
327330
function sepBy(elem: () => boolean, delim: string, kids: Cst[]): boolean {
328-
if (!elem()) return false;
331+
if (!elem()) return true; // the whole separated list is optional — zero elements is valid
329332
for (;;) {
330333
const sp = pos; const before = kids.length;
331334
if (!matchLit(delim, '$punct', kids)) { pos = sp; kids.length = before; break; }

test/portable-targets.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ const CASES: Case[] = [
165165
],
166166
reject: ['a ? b;', 'a ? : c;', 'in b;', 'a instanceof;'],
167167
},
168+
{
169+
// The no-`in` (suppress) context: a `for (binding in iterable)` head parses its binding
170+
// with the `in` led disabled, so `in` belongs to the for-head, not the binding.
171+
grammar: 'noinjs', path: '../examples/noinjs.ts',
172+
accept: [
173+
'for (x in y) z;', 'x in y;', 'for (a.b in c) d;', 'a in b in c;',
174+
'for ((x) in y) z;', 'for (x in y) a in b;', 'for (x in a in b) z;',
175+
'(a in b);', 'for (a in b) for (c in d) e;',
176+
],
177+
reject: ['for (x y) z;', 'for x in y;', 'for (in y) z;', 'for (x in) z;'],
178+
},
168179
];
169180

170181
const sortKeys = (o: unknown): unknown =>

0 commit comments

Comments
 (0)