From c0bc450bed6f917da3676acf357ccb60288a7e07 Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Thu, 15 Feb 2024 11:07:11 -0700 Subject: [PATCH] Update grammar to match peggy 4.0.0 --- .editorconfig | 4 + .gitignore | 1 + eslint.config.js | 1 + package-lock.json | 142 ++++++++----------------- src/ast.ts | 59 ++++++++++- src/parser.peggy | 124 ++++++++++++++++++++-- src/visitor.ts | 37 +++++-- test/fixtures/Makefile | 20 ++++ test/fixtures/comment.peggy | 7 ++ test/fixtures/fizzbuzz_import.peggy | 38 +++++++ test/fixtures/number.peggy | 5 + test/fixtures/space.peggy | 5 + test/visitor.test.js | 154 +++++++++++++++++++--------- tools/ast.js | 25 ++++- tsconfig.json | 1 + 15 files changed, 453 insertions(+), 170 deletions(-) create mode 100644 test/fixtures/Makefile create mode 100644 test/fixtures/comment.peggy create mode 100644 test/fixtures/fizzbuzz_import.peggy create mode 100644 test/fixtures/number.peggy create mode 100644 test/fixtures/space.peggy diff --git a/.editorconfig b/.editorconfig index 88b016a..af16735 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,7 @@ trim_trailing_whitespace = true charset = utf-8 indent_style = space indent_size = 2 + +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/.gitignore b/.gitignore index 3204179..002f460 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ lib/ node_modules/ pnpm-lock.yaml src/parser.js +test/fixtures/*.js diff --git a/eslint.config.js b/eslint.config.js index 1689f0b..6e58c25 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,7 @@ module.exports = [ "node_modules/**", "src/parser.d.ts", "src/parser.js", + "test/fixtures/*.js", ], }, require("@peggyjs/eslint-config/flat/js"), diff --git a/package-lock.json b/package-lock.json index 7abe23b..f482981 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "typescript": "5.3.3" }, "engines": { - "node": ">=14.20" + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -298,9 +298,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1194,26 +1194,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/copyfiles/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/copyfiles/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1714,48 +1694,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -1823,19 +1761,20 @@ } }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=12" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1853,16 +1792,26 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/globals": { @@ -2356,6 +2305,25 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -3012,26 +2980,6 @@ "concat-map": "0.0.1" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/src/ast.ts b/src/ast.ts index 63a1354..3d7b47a 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -7,7 +7,15 @@ import type * as EStree from "estree"; */ export const visitorKeys = { Program: ["body", "comments"], - grammar: ["topLevelInitializer", "initializer", "rules"], + grammar: ["imports", "topLevelInitializer", "initializer", "rules"], + grammar_import: ["what", "from"], + binding: [], + import_binding: ["binding"], + import_binding_all: ["binding"], + import_binding_default: ["binding"], + import_binding_rename: ["rename", "binding"], + import_module_specifier: ["before", "after"], + module_export_name: ["before", "after"], top_level_initializer: ["open", "code", "close", "semi"], initializer: ["code", "semi"], rule: ["name", "equals", "expression", "semi"], @@ -31,6 +39,7 @@ export const visitorKeys = { group: ["open", "expression", "close"], semantic_and: ["operator", "code"], semantic_not: ["operator", "code"], + library_ref: ["name", "library"], rule_ref: ["name"], literal: ["before", "after"], display: ["before", "after"], @@ -82,11 +91,47 @@ export interface Program extends BaseNode<"Program"> { } export interface Grammar extends BaseNode<"grammar"> { + imports: Import[]; topLevelInitializer?: TopLevelInitializer; initializer?: Initializer; rules: Rule[]; } +export interface Import extends BaseNode<"grammar_import"> { + what: ( + | ImportBinding + | ImportBindingAll + | ImportBindingDefault + | ImportBindingRename + )[]; + from: ImportModuleSpecifier; +} + +export interface ImportBinding extends BaseNode<"import_binding"> { + binding: Binding; +} + +export interface ImportBindingAll extends BaseNode<"import_binding_all"> { + binding: Binding; +} + +export interface ImportBindingDefault extends BaseNode<"import_binding_default"> { + binding: Binding; +} + +export interface ImportBindingRename extends BaseNode<"import_binding_rename"> { + rename: string; + binding: Binding; +} + +export interface Binding extends BaseNode<"binding"> { + id?: string; +} + +export type ImportModuleSpecifier = QuotedString<"import_module_specifier">; + +export type ModuleExportName = QuotedString<"module_export_name">; + export type TopLevelInitializer = BaseNode<"top_level_initializer"> & Bracketed & Coded & Terminated; export type Initializer = BaseNode<"initializer"> & Coded & Terminated; @@ -177,6 +222,7 @@ interface QuotedString extends ValueExpression { before: Punctuation; after: Punctuation; raw: string; + value: string; } export interface LiteralExpression extends QuotedString<"literal"> { @@ -256,6 +302,16 @@ export type Expression | SequenceExpression | SuffixedExpression; +export type ImportDtails + = Binding + | Import + | ImportBinding + | ImportBindingAll + | ImportBindingDefault + | ImportBindingRename + | ImportModuleSpecifier + | ModuleExportName; + export type Node = Boundaries | Boundary @@ -268,6 +324,7 @@ export type Node | DisplayName | Expression | Grammar + | ImportDtails | Initializer | Name | Program diff --git a/src/parser.peggy b/src/parser.peggy index 4b27f29..1d23b28 100644 --- a/src/parser.peggy +++ b/src/parser.peggy @@ -32,8 +32,6 @@ // // [1] http://www.ecma-international.org/publications/standards/Ecma-262.htm -// import {ImportDeclarations} from 'peggy/lib/parser.js' - {{ const OPS_TO_PREFIXED_TYPES = { @@ -92,19 +90,125 @@ Program } Grammar - // = imports:ImportDeclarations - = topLevelInitializer:(__ @TopLevelInitializer)? + = imports:ImportDeclarations + topLevelInitializer:(__ @TopLevelInitializer)? initializer:(__ @Initializer)? __ rules:(@Rule __)+ { return loc({ type: "grammar", - // imports, + imports, topLevelInitializer, initializer, rules, }); } +ImportDeclarations + = ImportDeclaration* + +ImportDeclaration + = __ "import" __ what:ImportClause __ from:FromClause (__ ";")? { + return loc({ + type: "grammar_import", + what, + from, + }); + } + / __ "import" __ from:ModuleSpecifier (__ ";")? { + return loc({ + type: "grammar_import", + what: [], + from, + }); + } // Intializers only + +ImportClause + = NameSpaceImport + / NamedImports + / first:ImportedDefaultBinding others:(__ "," __ @(NameSpaceImport / NamedImports))? { + if (!others) { + return [first]; + } + if (Array.isArray(others)) { + others.unshift(first); + return others; + } + return [first, others]; + } + +ImportedDefaultBinding + = binding:ImportedBinding { + return loc({ + type: 'import_binding_default', + binding, + }); + } + +NameSpaceImport + = "*" __ "as" __ binding:ImportedBinding { + return [loc({ + type: 'import_binding_all', + binding, + })]; + } + +NamedImports + = "{" __ "}" { return [] } // Can't have bare comma + / "{" __ @ImportsList __ ("," __)? "}" + +FromClause + = "from" __ @ModuleSpecifier + +ImportsList + = ImportSpecifier|1.., __ "," __| + +ImportSpecifier + = rename:ModuleExportName __ "as" __ binding:ImportedBinding { + return loc({ + type: 'import_binding_rename', + rename, + binding, + }) + } + / binding:ImportedBinding { + return loc({ + type: 'import_binding', + binding, + }); + } + +ModuleSpecifier + = module:StringLiteral { + return loc({ + type: 'import_module_specifier', + ...module, + }) + } + +ImportedBinding + = id:BindingIdentifier { + return loc({ + type: 'binding', + id + }); + } + +ModuleExportName + = IdentifierName + / name:StringLiteral { + return loc({ + type: 'module_export_name', + ...name, + }); + } + +BindingIdentifier = id:IdentifierName { + if (reservedWords.indexOf(id[0]) >= 0) { + error(`Binding identifier can't be a reserved word "${id[0]}"`, id[1]); + } + return id[0]; +} + TopLevelInitializer = open:OpenCurly code:CodeBlock close:CloseCurly semi:EOS { return loc({ @@ -323,7 +427,15 @@ PrimaryExpression } RuleReferenceExpression - = name:IdentifierName !( __ ( StringLiteral __ )? "=" ) { + = library:IdentifierName "." name:IdentifierName { + return loc({ + type: "library_ref", + name, + library, + libraryNumber: -1, + }); + } + / name:IdentifierName !(__ (StringLiteral __)? "=") { return loc({ type: "rule_ref", name }); } diff --git a/src/visitor.ts b/src/visitor.ts index 607c718..3ef352d 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -13,10 +13,20 @@ interface VisitorOptions { } type VisitorFunction = - (node: U, opts: VisitorOptions) => T | undefined; + (node: U, opts?: VisitorOptions) => T | undefined; interface VisitorFunctionMap { + "*"?: VisitorFunction; + Program?: VisitorFunction; grammar?: VisitorFunction; + grammar_import?: VisitorFunction; + binding?: VisitorFunction; + import_binding?: VisitorFunction; + import_binding_all?: VisitorFunction; + import_binding_default?: VisitorFunction; + import_binding_rename?: VisitorFunction; + import_module_specifier?: VisitorFunction; + module_export_name?: VisitorFunction; top_level_initializer?: VisitorFunction; initializer?: VisitorFunction; rule?: VisitorFunction; @@ -53,6 +63,14 @@ interface VisitorFunctionMap { "Program:exit"?: VisitorFunction; "grammar:exit"?: VisitorFunction; + "grammar_import:exit"?: VisitorFunction; + "binding:exit"?: VisitorFunction; + "import_binding:exit"?: VisitorFunction; + "import_binding_all:exit"?: VisitorFunction; + "import_binding_default:exit"?: VisitorFunction; + "import_binding_rename:exit"?: VisitorFunction; + "import_module_specifier:exit"?: VisitorFunction; + "module_export_name:exit"?: VisitorFunction; "top_level_initializer:exit"?: VisitorFunction; "initializer:exit"?: VisitorFunction; "rule:exit"?: VisitorFunction; @@ -87,9 +105,6 @@ interface VisitorFunctionMap { "Block:exit"?: VisitorFunction; "Line:exit"?: VisitorFunction; "*:exit"?: VisitorFunction; - - Program?(node: AST.Program): T | undefined; - "*"?(node: AST.Node, opts?: VisitorOptions): T | undefined; } /** @@ -97,11 +112,8 @@ interface VisitorFunctionMap { */ export class Visitor { public static visitorKeys: ESlint.SourceCode.VisitorKeys = AST.visitorKeys; - private functions: VisitorFunctionMap; - private star?: (node: AST.Node, opts?: VisitorOptions) => T | undefined; - private starExit?: VisitorFunction; /** @@ -121,9 +133,11 @@ export class Visitor { if (this.star) { parentResult = this.star(node, opts); } - const enterFun = this.functions[node.type]; + + // @ts-expect-error It looks like ts gets this wrong. + const enterFun: VisitorFunction | undefined + = this.functions[node.type]; if (enterFun) { - // @ts-expect-error Can't get correct node type here parentResult = enterFun(node, opts); } @@ -152,7 +166,9 @@ export class Visitor { } } - const exitFun = this.functions[`${node.type}:exit`]; + // @ts-expect-error It looks like ts gets this wrong. + const exitFun: VisitorFunction | undefined + = this.functions[`${node.type}:exit`]; if (this.starExit || exitFun) { if (!opts) { opts = { @@ -165,7 +181,6 @@ export class Visitor { opts.thisResult = parentResult; if (exitFun) { opts.thisResult = parentResult; - // @ts-expect-error Something went wrong with the types. exitFun(node, opts); } if (this.starExit) { diff --git a/test/fixtures/Makefile b/test/fixtures/Makefile new file mode 100644 index 0000000..09e5838 --- /dev/null +++ b/test/fixtures/Makefile @@ -0,0 +1,20 @@ +LIBS = space.js comment.js fizzbuzz.js + +.SUFFIXES: .ast.js .js .peggy +%.js: %.peggy + npx peggy --allowed-start-rules '*' $< +%.ast.js: %.peggy + ../../tools/ast.js $< > $@ + +.PHONY: all +all: fizzbuzz.js fizzbuzz_import.js +ast: fizzbuzz.ast.js fizzbuzz_import.ast.js + +fizzbuzz_import.js: fizzbuzz_import.peggy $(LIBS) + npx peggy $< + +comment.js: space.js + +.PHONY: clean +clean: + $(RM) *.js diff --git a/test/fixtures/comment.peggy b/test/fixtures/comment.peggy new file mode 100644 index 0000000..53aa18b --- /dev/null +++ b/test/fixtures/comment.peggy @@ -0,0 +1,7 @@ +import {_} from "./space.js" + +comment "comment" + = _ "/*" (!"*/" .)* "*/" _ + +trailing_comment + = _ "//" [^\n]+ diff --git a/test/fixtures/fizzbuzz_import.peggy b/test/fixtures/fizzbuzz_import.peggy new file mode 100644 index 0000000..0eb23bc --- /dev/null +++ b/test/fixtures/fizzbuzz_import.peggy @@ -0,0 +1,38 @@ +/* +This grammar aims to have one of every Peggy syntax. +It parses the output of a fizz-buzz (https://en.wikipedia.org/wiki/Fizz_buzz) +program (plus a few extensions) for correctness. +*/ +import comment from "./comment.js"; +import {_, trailing_comment as end_comment} from "./space.js"; +import {"number" as number} from "./number.js"; +import * as fzbz from "./fizzbuzz.js"; + +{{ +const NUMS = [3, 5]; +}} +{ +let currentNumber = (options.start == null) ? 1 : options.start|0; +} + +top = c:count|..| { return c.filter(fb => fb) } + +count + = end_comment "\n" { return } + / comment "\n" { return } + / comment? fb:line (comment / end_comment)? "\n" { + currentNumber++; + return fb; + } + +line + = @n:number &{ return (n === currentNumber) && NUMS.every(d => n % d) } + / fizzbuzz + / fizz + / buzz + +fizzbuzz = f:fizz _ b:buzz { return f + b } +fizz = @"fizz"i !{ return currentNumber % 3 } +buzz = @"buzz"i !{ return currentNumber % 5 } + +deep = fzbz.deep diff --git a/test/fixtures/number.peggy b/test/fixtures/number.peggy new file mode 100644 index 0000000..d3bc5b7 --- /dev/null +++ b/test/fixtures/number.peggy @@ -0,0 +1,5 @@ +// Arbitrary requirement needing & +number "number without trailing comment" + = "0x" n:$[0-9a-f]i+ &"\n" { return parseInt(n, 16) } + / n:$[0-9]+ &"\n" { return parseInt(n, 10) } + diff --git a/test/fixtures/space.peggy b/test/fixtures/space.peggy new file mode 100644 index 0000000..b4d5501 --- /dev/null +++ b/test/fixtures/space.peggy @@ -0,0 +1,5 @@ +_ "optional whitespace" + = $[ \t]* + +__ "required whitespace" + = $[ \t]+ diff --git a/test/visitor.test.js b/test/visitor.test.js index 5caf99b..34a664e 100644 --- a/test/visitor.test.js +++ b/test/visitor.test.js @@ -6,72 +6,124 @@ const { parseForESLint, visitor, } = require("../lib/index"); +const AST = require("../lib/ast"); + const path = require("path"); const fs = require("fs").promises; -const filePath = path.join(__dirname, "fixtures", "fizzbuzz.peggy"); +const fizzbuzz = path.join(__dirname, "fixtures", "fizzbuzz.peggy"); +const fizzbuzz_import = path.join(__dirname, "fixtures", "fizzbuzz_import.peggy"); -describe("visitor", () => { - it("visit all", async() => { - const source = await fs.readFile(filePath, "utf8"); - const { ast } = parseForESLint(source, { filePath }); - const v = new visitor.Visitor({ - "*": (node, opts) => { - if (opts) { - if (opts.array) { - return [...opts.parentResult, opts.name, node.type]; - } - return [...opts.parentResult, node.type]; +async function checkVisit(filePath) { + const source = await fs.readFile(filePath, "utf8"); + const { ast } = parseForESLint(source, { filePath }); + const v = new visitor.Visitor({ + "*": (node, opts) => { + if (opts) { + if (opts.array) { + return [...opts.parentResult, opts.name, node.type]; } - return ["Program"]; - }, - "*:exit": (node, opts) => { - if (typeof node !== "object") { - console.log("%o", opts); - throw new Error("fail"); - } - node.path = opts?.thisResult?.join("/"); - }, - }); - v.visit(ast); + return [...opts.parentResult, node.type]; + } + return ["Program"]; + }, + "*:exit": (node, opts) => { + if (typeof node !== "object") { + console.log("%o", opts); + throw new Error("fail"); + } + node.path = opts?.thisResult?.join("/"); + }, + }); + v.visit(ast); - // If needed: - // console.log(require("util").inspect(ast, { - // depth: Infinity, - // colors: process.stdout.isTTY, - // })); - - // Manually visit, and make sure each node has a path. - function testVisit(o) { - if (typeof o === "object") { - if (!o) { - return; + // If needed: + // console.log(require("util").inspect(ast, { + // depth: Infinity, + // colors: process.stdout.isTTY, + // })); + + // Manually visit, and make sure each node has a path. + function testVisit(o) { + if (typeof o === "object") { + if (!o) { + return; + } + if (Array.isArray(o)) { + for (const child of o) { + testVisit(child); + } + } else { + if (typeof o.path !== "string") { + console.log(o); } - if (Array.isArray(o)) { - for (const child of o) { + assert.equal(typeof o.path, "string"); + for (const [k, child] of Object.entries(o)) { + if (k !== "loc") { testVisit(child); } - } else { - if (typeof o.path !== "string") { - console.log(o); - } - assert.equal(typeof o.path, "string"); - for (const [k, child] of Object.entries(o)) { - if (k !== "loc") { - testVisit(child); - } - } } } } - // In case. - // console.log(require("util").inspect(ast, { depth: Infinity, colors: process.stdout.isTTY })); - testVisit(ast); + } + // In case. + // console.log(require("util").inspect(ast, { depth: Infinity, colors: process.stdout.isTTY })); + testVisit(ast); +} + +async function typeVisit(filePath) { + // Hope to catch issues where the VisitorFunctionMap isn't complete + // or has a typo. + const source = await fs.readFile(filePath, "utf8"); + const { ast } = parseForESLint(source, { filePath }); + + const stack = []; + const keyFuncs = { + // Star rules happen before specfic rules + "*": node => { + stack.push(node); + }, + // Star-exit rules happen after specific rules. + "*:exit": node => { + stack.pop(node); + }, + }; + Object.keys(AST.visitorKeys).forEach(k => { + keyFuncs[k] = node => { + assert.equal(node.type, k); + assert.equal(stack[stack.length - 1], node); // NOT deep equal + }; + keyFuncs[`${k}:exit`] = node => { + assert.equal(node.type, k); + assert.equal(stack[stack.length - 1], node); // NOT deep equal + }; + }); + + const v = new visitor.Visitor(keyFuncs); + v.visit(ast); + assert.equal(stack.length, 0); +} + +describe("visitor", () => { + it("visits all in fizzbuzz.peggy", async() => { + await checkVisit(fizzbuzz); + }); + + it("visits all in fizzbuzz_import.peggy", async() => { + await checkVisit(fizzbuzz_import); + }); + + it("checks types when visiting fizzbuzz.peggy", async() => { + await typeVisit(fizzbuzz); + }); + + it("checks types when visiting fizzbuzz_import.peggy", async() => { + await typeVisit(fizzbuzz_import); }); it("functions", async() => { - const source = await fs.readFile(filePath, "utf8"); - const { ast } = parseForESLint(source, { filePath }); + const source = await fs.readFile(fizzbuzz, "utf8"); + const { ast } = parseForESLint(source, { filePath: fizzbuzz }); const functions = []; function addFunction(predicate, params, body) { diff --git a/tools/ast.js b/tools/ast.js index d461d53..83e93f6 100755 --- a/tools/ast.js +++ b/tools/ast.js @@ -4,17 +4,34 @@ const { parse } = require("../lib/parser"); const fs = require("fs"); const util = require("util"); +const { Visitor } = require("../lib/visitor.js"); const grammarSource = process.argv[2]; const text = fs.readFileSync(grammarSource, "utf8"); try { - console.log(util.inspect(parse(text, { - grammarSource, - }), { + const node = parse(text, { grammarSource }); + const v = new Visitor({ + "*": n => { + delete n.loc; + delete n.range; + }, + }); + v.visit(node); + + console.log(`\ +/* eslint-disable @stylistic/array-bracket-spacing */ +/* eslint-disable @stylistic/comma-dangle */ +/* eslint-disable @stylistic/quotes */ +/* eslint-disable @stylistic/operator-linebreak */ +"use strict"; +`); + console.log("module.exports =", util.inspect(node, { depth: Infinity, + maxArrayLength: Infinity, + maxStringLength: Infinity, colors: process.stdout.isTTY, - })); + }) + ";"); } catch (er) { if (er.format) { console.log(er.format([{ source: grammarSource, text }])); diff --git a/tsconfig.json b/tsconfig.json index 54cf9a7..bdbc558 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "noErrorTruncation": true, "allowJs": true, "declaration": true, "declarationMap": false,