From 1dc4580b1fb367ec24e3e238070e7e7abf04f515 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 25 Nov 2022 13:52:54 -0800 Subject: [PATCH 01/28] feat: ESLint Language Plugins --- designs/2022-languages/README.md | 290 +++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 designs/2022-languages/README.md diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md new file mode 100644 index 00000000..f58bb73a --- /dev/null +++ b/designs/2022-languages/README.md @@ -0,0 +1,290 @@ +- Repo: eslint/eslint +- Start Date: 2022-11-25 +- RFC PR: (leave this empty, to be filled in later) +- Authors: Nicholas C. Zakas + +# ESLint Language Plugins + +## Summary + +This proposal provides a way for plugin authors to define parsers, rules, etc., for any arbitrary programming languages. This would allow ESLint to work on any type of programming language for which there is a plugin defined, not just for JavaScript and its variants. + +## Motivation + +Over the nine years ESLint has been in existence, it has grown into being not just the most popular JavaScript linter, but arguably the most popular linter for any programming language. Twitter is filled with people lamenting that they don't have an "ESLint for X", where X is some other file type they're working with. Numerous projects have popped up that mimic a lot of what ESLint does at it's core: + +* [Stylelint](https://stylelint.io/); +* [Markdownlint](https://github.com/DavidAnson/markdownlint) + +In addition, there are numerous projects that hack around other languages to make them work in ESLint, such as: + +* [GraphQL-ESLint](https://github.com/B2o5T/graphql-eslint) +* [eslint-plugin-json](https://www.npmjs.com/package/eslint-plugin-json) +* [typescript-eslint](https://typescript-eslint.io) + +Instead of forcing others to rewrite what is core linting functionality, or asking people to produce an ESTree format AST for some language other than JavaScript, we can make everyone's lives easier by providing formal language support such that languages can be plugged in to ESLint easily. + +## Detailed Design + +This proposal consists of the following changes: + +1. Allows plugins to publish languages in addition to rules, processors, etc. +1. Allows end users to select which language to use in their config files. +1. Defines an API for defining languages. +1. Changes the ESLint core to consume language definitions instead of assuming JavaScript is always used. + +**Prerequisites:** This proposal requires the flat config system and cannot be used with the eslintrc config system. + +**Note:** This design is intended to work both in the current version of ESLint and also in the "rewrite" version, which I'll refer to as ESLint X to distinguish between the two. Not all features of this proposal will be available in both versions. + +### Languages in plugins and configs + +Languages can be defined in plugins by using the `languages` key and giving the language a name. This allows multiple languages to be contained in a single plugin if desired. For example: + +```js +// inside a plugin called eslint-plugin-lang +exports default { + + languages: { + lang1: { + // language object + }, + lang2: { + // language object + } + }, + processors: { + // .. + }, + rules: { + // ... + } +}; +``` + +Here, `lang1` and `lang2` are languages defined by the plugin. End users can specify which language to use via the `language` key in a config object, such as: + + +```js +// inside eslint.config.js +import lang from "eslint-plugin-lang"; + +exports default [ + { + plugins: { + lang + }, + files: ["**/*.lang"], + language: "lang/lang1", + languageOptions: { + // options + } + } +]; +``` + +Here, `"lang/lang1"` refers to the `lang1` language as exported from `eslint-plugin-lang`. Anything inside of `languageOptions` is then handled by `lang1` rather than by ESLint itself. + +### Language Definition Objects + +Each language definition object must implement the following interface: + +```ts +interface ESLintLanguage { + + /** + * Indicates how ESLint should read the file. + */ + fileType: "text" | "binary"; + + /** + * Validates languageOptions for this language. + */ + validateOptions(options: LanguageOptions): void; + + /** + * Parses the given file input into its component parts. + */ + parse(file: File, env: LanguageContext): ParseResult | Promise; + + /** + * Creates SourceCode object that ESLint uses to work with a file. + */ + createSourceCode(input: ParseResult, env: LanguageContext): SourceCode | Promise; + +} + +// Supplemental interfaces + +interface LanguageOptions { + // language-defined +} + +interface File { + path: string; + body: string | ArrayBuffer +} + +interface LanguageContext { + options: LanguageOptions; +} + +interface ParseResult { + ast: ASTNode; + body: string | ArrayBuffer; +} +``` + +At a high-level, ESLint uses the methods on a language object in the following way: + +* During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that +* When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. +* After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. +* The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. + + +### The `validateOptions()` Method + +The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method. + +The `validateOptions()` method must throw an error if any part of the `languageOptions` object is invalid. + +No specific `languageOptions` keys will be required for languages. The `parser` and `parserOptions` keys are unique to JavaScript and may not make sense for other languages. + +#### The `parse()` Method + +The `parse()` method receives the file information and information about the ESLint context in order to correctly parse the file. This is a raw parse, meaning that `parse()` should do nothing more than convert the file into an abstract format without doing any postprocessing. This is important so that ESLint can tell how long parsing takes vs. other operations. (One problem with the current `parseForESLint()` approach is that we lose visibility into the true parsing time.) + +The `parse()` method is expected to return an object with at least `ast` and `body` properties; the object may also have other properties as necessary for the language to properly create a `SourceCode` object later. The return value from `parse()` is passed directly into `createSourceCode()`. + +**Async:** For use with current ESLint, `parse()` must be synchronous; ESLint X will allow this method to be asynchronous. + +#### The `createSourceCode()` Method + +The `createSourceCode()` method is intended to create an instance of `SourceCode` suitable for ESLint and rule developers to use to work with the file. As such, it may perform postprocessing of a parse result in order to get it into a format that is easier to use. + +### The `SourceCode` Object + +ESLint already has a `SourceCode` class that is used to [postprocess and organize the AST data](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/source-code/source-code.js#L149) for JavaScript files. This proposal extends the intended purpose of `SourceCode` such that it encapsulates all functionality related to a single file, including traversal. As such, `SourceCode` objects must implement the following interface: + +```ts +interface SourceCode { + + /** + * Root of the AST. + */ + ast: ASTNode; + + /** + * The body of the file that you'd like rule developers to access. + */ + body: string | ArrayBuffer; + + /** + * Traversal of AST. + */ + traverse(): Iterable; + + /** + * Determines if a node matches a given selector. + */ + match(node: ASTNode, selector: string): boolean; +} + +interface TraversalStep { + type: "call" | "visit"; + target: string | ASTNode; + args: object; +} +``` + +Other than these three members of the interface, languages define any additional methods or properties that they need to provide to rule developers. For instance, +the JavaScript `SourceCode` object currently has methods allowing retrieval of tokens, comments, and lines. It is up to the individual language object implementations to determine what additional properties and methods may be required inside of rules. + + +### Core Changes + +TODO + +The functionality in `context.getScope()` will need to move to `SourceCode#getScope(node)`, where `node` must be passed in to retrieve the scope. We will need to map `context.getScope()` to call `SourceCode#getScope(node)` during the transition period. + + +## Documentation + + + +## Drawbacks + + + +## Backwards Compatibility Analysis + + + +## Alternatives + + + +## Open Questions + + + +## Help Needed + + + +## Frequently Asked Questions + +**Why allow binary files?** + +I think there may be a use in validating non-text files such as images, videos, and WebAssembly files. I may be wrong, but by allowing languages to specify that they'd prefer to receive the file in binary, it at least opens up the possibility that people may create plugins that can validate binary files in some way. + +**Why not have `parse()` return a `SourceCode` instead of needing to call another method?** + +It's important that we are able to determine the amount of time it takes to parse a file in order to optimize performance. Combining the parsing with creating a `SourceCode` object, which may require additional processing of the parse result, means obscuring this important number. + +Additionally, `createSourceCode()` allows integrators to do their own parsing and create their own `SourceCode` instances without requiring the use of a specific `parse()` method. + +## Related Discussions + + From 614bef2fb5f5441e2477350e7791c795a388fd4d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 28 Nov 2022 12:03:20 -0800 Subject: [PATCH 02/28] More details --- designs/2022-languages/README.md | 265 ++++++++++++++++++++++++++++--- 1 file changed, 242 insertions(+), 23 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index f58bb73a..f8216f78 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -3,7 +3,7 @@ - RFC PR: (leave this empty, to be filled in later) - Authors: Nicholas C. Zakas -# ESLint Language Plugins +# ESLint X Language Plugins ## Summary @@ -110,7 +110,7 @@ interface ESLintLanguage { /** * Creates SourceCode object that ESLint uses to work with a file. */ - createSourceCode(input: ParseResult, env: LanguageContext): SourceCode | Promise; + createSourceCode(file: File, input: ParseResult, env: LanguageContext): SourceCode | Promise; } @@ -121,7 +121,29 @@ interface LanguageOptions { } interface File { + + /** + * The path that ESLint uses for this file. May be a virtual path + * if it was returned by a processor. + */ path: string; + + /** + * The path to the file on disk. This always maps directly to a file + * regardless of whether it was returned from a processor. + */ + physicalPath: string; + + /** + * Indicates if the original source contained a byte-order marker. + * ESLint strips the BOM from the `body`, but this info is needed + * to correctly apply autofixing. + */ + bom: boolean; + + /** + * The body of the file to parse. + */ body: string | ArrayBuffer } @@ -142,10 +164,9 @@ At a high-level, ESLint uses the methods on a language object in the following w * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. - ### The `validateOptions()` Method -The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method. +The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of the schema](https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/config/flat-config-schema.js#L445). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). The `validateOptions()` method must throw an error if any part of the `languageOptions` object is invalid. @@ -157,11 +178,17 @@ The `parse()` method receives the file information and information about the ESL The `parse()` method is expected to return an object with at least `ast` and `body` properties; the object may also have other properties as necessary for the language to properly create a `SourceCode` object later. The return value from `parse()` is passed directly into `createSourceCode()`. +This method will be called [inside of `Linter`](https://github.com/eslint/eslint/blob/28d190264017dbaa29f2ab218f73b623143cd1af/lib/linter/linter.js#L805) and will require that the language object be passed into the private `parse()` function. + **Async:** For use with current ESLint, `parse()` must be synchronous; ESLint X will allow this method to be asynchronous. #### The `createSourceCode()` Method -The `createSourceCode()` method is intended to create an instance of `SourceCode` suitable for ESLint and rule developers to use to work with the file. As such, it may perform postprocessing of a parse result in order to get it into a format that is easier to use. +The `createSourceCode()` method is intended to create an instance of `SourceCode` suitable for ESLint and rule developers to use to work with the file. As such, it may perform postprocessing of a parse result in order to get it into a format that is easier to use. + +This method will be called [inside of `Linter`](https://github.com/eslint/eslint/blob/28d190264017dbaa29f2ab218f73b623143cd1af/lib/linter/linter.js#L828) and will require that the language object be passed into the private `parse()` function. + +**Async:** For use with current ESLint, `createSourceCode()` must be synchronous; ESLint X will allow this method to be asynchronous. ### The `SourceCode` Object @@ -181,25 +208,164 @@ interface SourceCode { body: string | ArrayBuffer; /** - * Traversal of AST. + * Determines if a node matches a given selector. */ - traverse(): Iterable; + match(node: ASTNode, selector: string): boolean; /** - * Determines if a node matches a given selector. + * Traversal of AST. */ - match(node: ASTNode, selector: string): boolean; + traverse(): Iterable; +} + +type TraversalStep = VisitTraversalStep | CallTraversalStep; + +interface VisitTraversalStep { + type: "visit"; + target: ASTNode; + phase: "enter" | "exit"; + args: Array; +} + +interface CallTraversalStep { + type: "call"; + target: string; + phase: string | null | undefined; + args: Array; +} +``` + +Other than these interface members, languages may define any additional methods or properties that they need to provide to rule developers. For instance, +the JavaScript `SourceCode` object currently has methods allowing retrieval of tokens, comments, and lines. It is up to the individual language object implementations to determine what additional properties and methods may be required inside of rules. (We may want to provide some best practices for other methods, but they won't be required.) + +#### The `SourceCode#match()` Method + +ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match visitor patterns to nodes. `esquery` is defined for use specifically with ESTree-compatible AST structures, which means that it cannot work for other structures without modification. We need a generic way to ensure that a given pattern in a visitor matches a given node, and that is the purpose of the `SourceCode#match()` method. + +For JavaScript, `SourceCode#match()` will simply use `esquery`; for other languages, they will be able to either implement their own CSS-query utilities or else just stick with node names to get started. (ESLint initially only matched the `type` property; `esquery` was added several years later.) + +We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use `SourceCode#match()` instead of `esquery`. + +#### The `SoureCode#traverse()` Method + +Today, ESLint uses a [custom traverser](https://github.com/eslint/eslint/blob/b3a08376cfb61275a7557d6d166b6116f36e5ac2/lib/shared/traverser.js) based on data from [`eslint-visitor-keys`](https://npmjs.com/package/eslint-visitor-keys). All of this is specific to an ESTree-compatible AST structure. There are two problems with this: + +1. We cannot traverse anything other than ESTree structures. +1. Because this traversal is synchronous, rules must always be synchronous. + +The `SourceCode#traverse()` method solves these problems by allowing each `SourceCode` instance to specify the traversal order through an iterable. Returning an iterable means several things: + +1. The method can be a generator or just return an array. This gives maximum flexibility to language authors. +1. The ESLint core can move step by step through the traversal, waiting for asynchronous calls to return before moving to the next node. This will ultimately enable async rules in ESLint X. +1. It's possible to add method calls into the traversal, such as the current `onCodePathStart()` method for JavaScript, in a standard way that is available to other languages. +1. If the `SourceCode` object ends up doing more than one traversal of the same AST (which is how ESLint currently work), it can cache the traversal steps in an array to reuse multiple times, saving compute time. + +As an example, the [current traversal](https://github.com/eslint/eslint/blob/b3a08376cfb61275a7557d6d166b6116f36e5ac2/lib/shared/traverser.js#L127) can be rewritten to look like this: + +```js +class Traverser { + + // methods skipped + + *_traverse(node, parent) { + if (!isNode(node)) { + return; + } + + this._current = node; + this._skipped = false; + yield { + type: "visit", + target: node, + phase: "enter", + args: [node, parent] + } + + if (!this._skipped && !this._broken) { + const keys = getVisitorKeys(this._visitorKeys, node); + + if (keys.length >= 1) { + this._parents.push(node); + for (let i = 0; i < keys.length && !this._broken; ++i) { + const child = node[keys[i]]; + + if (Array.isArray(child)) { + for (let j = 0; j < child.length && !this._broken; ++j) { + yield *this._traverse(child[j], node); + } + } else { + yield *this._traverse(child, node); + } + } + this._parents.pop(); + } + } + + if (!this._broken) { + yield { + type: "visit", + target: node, + phase: "exit", + args: [node, parent] + } + } + + this._current = parent; + } + } +``` + +Note that we use this data inside of [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js) to make the calls into the visitor objects. -interface TraversalStep { - type: "call" | "visit"; - target: string | ASTNode; - args: object; +Using `SourceCode#traverse()`, we will be able to traverse an AST like this: + +```js +// Example only -- not intended to be the final implementation +for (const step of sourceCode.traverse()) { + + switch (step.type) { + + // visit a node + case "node": { + rules.forEach(rule => { + Object.keys(rule).forEach(key => { + + // match the selector + if (sourceCode.match(step.target, key)) { + + // account for enter and exit phases + if (key.endsWith(":exit")) { + if (step.phase === "exit") { + rule[key](...step.args) + } + } else { + rule[key](...step.args) + } + } + }); + }); + break; + } + + // call a method + case "call": { + rules.forEach(rule => { + Object.keys(rule).forEach(key => { + if (step.target === key) { + rule[key](...step.args) + } + }); + }); + break; + } + default: + throw new Error(`Invalid step type "${ step.type }" found.`); + } } ``` -Other than these three members of the interface, languages define any additional methods or properties that they need to provide to rule developers. For instance, -the JavaScript `SourceCode` object currently has methods allowing retrieval of tokens, comments, and lines. It is up to the individual language object implementations to determine what additional properties and methods may be required inside of rules. +In ESLint X, we can swith the `for-of` loop for a `for await-of` loop in order to allow asynchronous traversal, as well. ### Core Changes @@ -208,6 +374,31 @@ TODO The functionality in `context.getScope()` will need to move to `SourceCode#getScope(node)`, where `node` must be passed in to retrieve the scope. We will need to map `context.getScope()` to call `SourceCode#getScope(node)` during the transition period. +#### Rule Context + +The rule context object, which is passed into rules as `context`, will need to be significantly altered to support other languages. All of the methods that relate specifically to language features will need to move into `SourceCode`, leaving us with a rule context object that follows this interface: + +```ts +interface RuleContext { + + languageOptions: LanguageOptions; + settings: object; + + getCwd(): string; + getFilename(): string; + getPhysicalFilename(): string; + getSourceCode(): string; +} +``` + +We should strictly use this interface for all non-JavaScript languages from the start. For JavaScript, we will need to deprecate the following methods and redirect them to the `SourceCode` object: + +* `getAncestors()` +* `getDeclaredVariables()`, +* `getScope()` +* `parserOptions` +* `parserPath` +* `parserServices` ## Documentation @@ -248,16 +439,44 @@ The functionality in `context.getScope()` will need to move to `SourceCode#getSc ## Open Questions - ## Help Needed From f1c2e77603b3bbb4c64556b3799e3b48ffdb7bb8 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 2 Dec 2022 11:44:59 -0800 Subject: [PATCH 03/28] More details --- designs/2022-languages/README.md | 78 ++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index f8216f78..9a54a759 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -216,6 +216,11 @@ interface SourceCode { * Traversal of AST. */ traverse(): Iterable; + + /** + * Return all of the inline areas where ESLint should be disabled/enabled. + */ + getDisableDirectives(): Array; } type TraversalStep = VisitTraversalStep | CallTraversalStep; @@ -233,6 +238,31 @@ interface CallTraversalStep { phase: string | null | undefined; args: Array; } + +interface DisableDirective { + type: "enable" | "disable"; + ruleId: string; + + /** + * The point from which the directive should be applied. + */ + from: Location; + + /** + * Location of the comment. + */ + loc: LocationRange; +} + +interface LocationRange { + start: Location; + end: Location; +} + +interface Location { + line: number; + column: number; +} ``` Other than these interface members, languages may define any additional methods or properties that they need to provide to rule developers. For instance, @@ -321,7 +351,7 @@ Note that we use this data inside of [`NodeEventGenerator`](https://github.com/e Using `SourceCode#traverse()`, we will be able to traverse an AST like this: ```js -// Example only -- not intended to be the final implementation +// Example only!! -- not intended to be the final implementation for (const step of sourceCode.traverse()) { switch (step.type) { @@ -337,10 +367,12 @@ for (const step of sourceCode.traverse()) { // account for enter and exit phases if (key.endsWith(":exit")) { if (step.phase === "exit") { - rule[key](...step.args) + + // async visitor methods!! + await rule[key](...step.args) } } else { - rule[key](...step.args) + await rule[key](...step.args) } } }); @@ -353,7 +385,7 @@ for (const step of sourceCode.traverse()) { rules.forEach(rule => { Object.keys(rule).forEach(key => { if (step.target === key) { - rule[key](...step.args) + await rule[key](...step.args) } }); }); @@ -367,13 +399,21 @@ for (const step of sourceCode.traverse()) { In ESLint X, we can swith the `for-of` loop for a `for await-of` loop in order to allow asynchronous traversal, as well. +#### The `SoureCode#getDisableDirectives()` Method + +ESLint has all kinds of disable directives to use inside of JavaScript. It's impossible for the ESLint core to know how disable directives might be formatted or used in other languages, so we need to abstract this out and delegate it to the `SourceCode` object. The `SourceCode#getDisableDirectives()` method returns an array of disable directive objects indicating when each rule is disabled and enabled. + +The ESLint core will then use the disable directive information to: + +* Filter out violations appropriately. +* Report any unused disable directives. + +This functionality currently lives in [`apply-disable-directives.js`](https://github.com/eslint/eslint/blob/0311d81834d675b8ae7cc92a460b37115edc4018/lib/linter/apply-disable-directives.js). ### Core Changes TODO -The functionality in `context.getScope()` will need to move to `SourceCode#getScope(node)`, where `node` must be passed in to retrieve the scope. We will need to map `context.getScope()` to call `SourceCode#getScope(node)` during the transition period. - #### Rule Context The rule context object, which is passed into rules as `context`, will need to be significantly altered to support other languages. All of the methods that relate specifically to language features will need to move into `SourceCode`, leaving us with a rule context object that follows this interface: @@ -383,11 +423,27 @@ interface RuleContext { languageOptions: LanguageOptions; settings: object; + options: Array; + id: string; getCwd(): string; getFilename(): string; getPhysicalFilename(): string; - getSourceCode(): string; + getSourceCode(): SourceCode; + report(violation: Violation): void; +} + +interface Violation { + messageId: string; + data: object; + loc: LocationRange; + suggest: Array; + fix(fixer: Fixer): Iterable; +} + +interface Suggestion { + desc: string; + fix(fixer: Fixer): Iterable; } ``` @@ -396,9 +452,15 @@ We should strictly use this interface for all non-JavaScript languages from the * `getAncestors()` * `getDeclaredVariables()`, * `getScope()` + +These properties we can remove once we remove the eslintrc config system, so they will still show up for JavaScript but won't be included for non-JavaScript: + * `parserOptions` * `parserPath` -* `parserServices` + +The `parserServices` property will need to remain for JavaScript linting until we deprecate the `parseForESLint()` method at some point in the future. + +**Note:** I have purposely removed the `message` property from the `Violation` interface. I'd like to move to just one way of supplying messages inside of rules and have that be `messageId`. This will also reduce the complexity of processing the violations. ## Documentation From 616e0e4e8e1a3cbdce82d20edb8df8499aacac7d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 2 Dec 2022 12:46:23 -0800 Subject: [PATCH 04/28] Notes on JS changes --- designs/2022-languages/README.md | 67 ++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 9a54a759..429a7d2f 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -85,7 +85,7 @@ exports default [ Here, `"lang/lang1"` refers to the `lang1` language as exported from `eslint-plugin-lang`. Anything inside of `languageOptions` is then handled by `lang1` rather than by ESLint itself. -### Language Definition Objects +### Language Definition Object Each language definition object must implement the following interface: @@ -164,7 +164,7 @@ At a high-level, ESLint uses the methods on a language object in the following w * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. -### The `validateOptions()` Method +#### The `validateOptions()` Method The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of the schema](https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/config/flat-config-schema.js#L445). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). @@ -410,11 +410,57 @@ The ESLint core will then use the disable directive information to: This functionality currently lives in [`apply-disable-directives.js`](https://github.com/eslint/eslint/blob/0311d81834d675b8ae7cc92a460b37115edc4018/lib/linter/apply-disable-directives.js). -### Core Changes +### Extracting JavaScript Functionality + +An important part of this process will be extracting all of the JavaScript functionality for the ESLint core and placing it in a language object. As a first step, that language object will live in the main ESLint repository so we can easily test and make changes to ensure backwards compatibility. Eventually, though, I'd like to move this functionality into its own `eslint/js` repo. + +#### Update Default Config + +TODO + +#### Move Traversal into `SourceCode` + +TODO + +#### Split Disable Directive Functionality + +Between `SourceCode` and `Linter`. + +TODO + +#### Move Rule Context Methods to `SourceCode` + +TODO + +For JavaScript, we will need to deprecate the following methods and redirect them to the `SourceCode` object: + +* `getAncestors()` +* `getDeclaredVariables()`, +* `getScope()` + +The following properties we can remove once we remove the eslintrc config system, so they will still show up for JavaScript but won't be included for non-JavaScript: + +* `parserOptions` +* `parserPath` + +The `parserServices` property will need to remain for JavaScript linting until we deprecate the `parseForESLint()` method at some point in the future. + +#### Update Rules to New APIs TODO -#### Rule Context + +### Core Changes + +In order to make all of this work, we'll need to make the following changes in the core: + +1. `NodeEventGenerator` will need to be updated to call `SourceCode#traverse()`. +1. `FlatConfigArray` will need to be updated to define the `language` key and to delegate validation of `languageOptions` to the language. +1. `Linter` will need to be updated to honor the `language` key and to use JS-specific functionality where we need to provide backwards compatibility for existing JS rules. There will be a lot of code removed and delegated to the language being used, including filtering of violation via disable directives, traversing the AST, and formulating the final violation list. +1. The `context` object passed to rules will need to be updated so it works for all languages. +1. `RuleTester` will need to be updated to support passing in `language`. + +#### Updating Rule Context The rule context object, which is passed into rules as `context`, will need to be significantly altered to support other languages. All of the methods that relate specifically to language features will need to move into `SourceCode`, leaving us with a rule context object that follows this interface: @@ -447,18 +493,7 @@ interface Suggestion { } ``` -We should strictly use this interface for all non-JavaScript languages from the start. For JavaScript, we will need to deprecate the following methods and redirect them to the `SourceCode` object: - -* `getAncestors()` -* `getDeclaredVariables()`, -* `getScope()` - -These properties we can remove once we remove the eslintrc config system, so they will still show up for JavaScript but won't be included for non-JavaScript: - -* `parserOptions` -* `parserPath` - -The `parserServices` property will need to remain for JavaScript linting until we deprecate the `parseForESLint()` method at some point in the future. +We should strictly use this interface for all non-JavaScript languages from the start. **Note:** I have purposely removed the `message` property from the `Violation` interface. I'd like to move to just one way of supplying messages inside of rules and have that be `messageId`. This will also reduce the complexity of processing the violations. From 7a91235bdd95ca8cd65c35aa0339c345baea7b11 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 6 Dec 2022 11:32:38 -0800 Subject: [PATCH 05/28] Finished the RFC --- designs/2022-languages/README.md | 126 ++++++++++++++++--------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 429a7d2f..2903184e 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -3,7 +3,7 @@ - RFC PR: (leave this empty, to be filled in later) - Authors: Nicholas C. Zakas -# ESLint X Language Plugins +# ESLint Language Plugins ## Summary @@ -274,7 +274,7 @@ ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match vi For JavaScript, `SourceCode#match()` will simply use `esquery`; for other languages, they will be able to either implement their own CSS-query utilities or else just stick with node names to get started. (ESLint initially only matched the `type` property; `esquery` was added several years later.) -We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use `SourceCode#match()` instead of `esquery`. +We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use `SourceCode#match()` instead of `esquery`. This will require significant refactoring. #### The `SoureCode#traverse()` Method @@ -351,7 +351,7 @@ Note that we use this data inside of [`NodeEventGenerator`](https://github.com/e Using `SourceCode#traverse()`, we will be able to traverse an AST like this: ```js -// Example only!! -- not intended to be the final implementation +// Simplified example only!! -- not intended to be the final implementation for (const step of sourceCode.traverse()) { switch (step.type) { @@ -408,57 +408,53 @@ The ESLint core will then use the disable directive information to: * Filter out violations appropriately. * Report any unused disable directives. -This functionality currently lives in [`apply-disable-directives.js`](https://github.com/eslint/eslint/blob/0311d81834d675b8ae7cc92a460b37115edc4018/lib/linter/apply-disable-directives.js). +This functionality currently lives in [`apply-disable-directives.js`](https://github.com/eslint/eslint/blob/0311d81834d675b8ae7cc92a460b37115edc4018/lib/linter/apply-disable-directives.js) and will have to be updated to call `SourceCode#getDisableDirectives()`. ### Extracting JavaScript Functionality An important part of this process will be extracting all of the JavaScript functionality for the ESLint core and placing it in a language object. As a first step, that language object will live in the main ESLint repository so we can easily test and make changes to ensure backwards compatibility. Eventually, though, I'd like to move this functionality into its own `eslint/js` repo. -#### Update Default Config +#### Update Default Flat Config -TODO +The default flat config will need to be updated to specify a default `language` to use. We would need to hardcode the language for eslintrc configs as there won't be a way to set that in the config itself. (Likely, `config.language || defaultLanguage` in `Linter`.) #### Move Traversal into `SourceCode` -TODO +The JavaScript-specific traversal functionality that currently lives in [`linter.js`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L1140-L1158) will need to move into `SourceCode#traverse()`. #### Split Disable Directive Functionality -Between `SourceCode` and `Linter`. +The [`applyDisableDirectives()`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L1427-L1434) method is called inside of `Linter`, and if `Linter` calls `SourceCode#getDisableDirectives()`, we can likely reuse a lot of the existing functionality with some minor tweaks. -TODO +The [`getDirectiveComments()`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L367) function will move into `SourceCode` to postprocess the AST with all directive comments and then expose just the disable comments through `SourceCode#getDisableDirectives()`. -#### Move Rule Context Methods to `SourceCode` +#### Move Rule Context Methods and Properties to `SourceCode` -TODO - -For JavaScript, we will need to deprecate the following methods and redirect them to the `SourceCode` object: +For JavaScript, we will need to deprecate the following methods and properties and redirect them to the `SourceCode` object: * `getAncestors()` * `getDeclaredVariables()`, * `getScope()` +* `parserServices` The following properties we can remove once we remove the eslintrc config system, so they will still show up for JavaScript but won't be included for non-JavaScript: * `parserOptions` * `parserPath` -The `parserServices` property will need to remain for JavaScript linting until we deprecate the `parseForESLint()` method at some point in the future. - #### Update Rules to New APIs -TODO - +With a new Rule Context, we should also start converting existing rules over to the new APIs as they move from `context` onto `SourceCode`. ### Core Changes In order to make all of this work, we'll need to make the following changes in the core: -1. `NodeEventGenerator` will need to be updated to call `SourceCode#traverse()`. +1. [`linter.js`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L1140-L1158) will need to be updated to call `SourceCode#traverse()`. 1. `FlatConfigArray` will need to be updated to define the `language` key and to delegate validation of `languageOptions` to the language. 1. `Linter` will need to be updated to honor the `language` key and to use JS-specific functionality where we need to provide backwards compatibility for existing JS rules. There will be a lot of code removed and delegated to the language being used, including filtering of violation via disable directives, traversing the AST, and formulating the final violation list. 1. The `context` object passed to rules will need to be updated so it works for all languages. -1. `RuleTester` will need to be updated to support passing in `language`. +1. `RuleTester` will need to be updated to support passing in `language`. We should also updated `RuleTester` to warn about the newly-deprecated `context` methods and properties. #### Updating Rule Context @@ -499,62 +495,54 @@ We should strictly use this interface for all non-JavaScript languages from the ## Documentation - +Most existing end-user facing documentation will remain unchanged. -## Drawbacks +Plugin developer documentation will need to be updated: +* The documentation on writing new rules needs to be updated to include API changes. +* A new page on writing languages for ESLint will need to be written. This will need to be quite extensive. - +1. **Performance.** There will likely be a small performance penalty from this refactoring. Moving to iterators from standard loops seems to incur a slowdown. My hope is that we can mitigate the effects of this performance hit by refactoring code in other ways. +1. **eslintrc Compatibility.** Unfortunately, there's no easy way to make this functionality available to eslintrc config users. When implemented, eslintrc users will be left without the ability to use other languages. ## Backwards Compatibility Analysis - +This proposal is 100% backwards compatible in the short-term, and all breaking changes are optional and can be phased in over time to avoid disrupting the ecosystem. -## Alternatives +This proposal does not require any changes from end-users. - +1. We could continue using the `parserForESLint()` approach to support other languages. +1. We could choose to not support languages other than JavaScript. ## Open Questions -**Should `RuleContext` just have properties instead of a mix of properties and methods?** +### Should `RuleContext` have mostly properties instead of a mix of properties and methods? -With the proposal as-is, `RuleContext` will have to properties and four methods, but all of the methods just return a value. If we are looking at a larger rewrite, should we create a cleaner interface with just properties? Such as: +With the proposal as-is, `RuleContext` will have four properties and five methods, but four of the five methods just return a value. If we are looking at a larger rewrite, should we create a cleaner interface with just properties? Such as: ```ts interface RuleContext { languageOptions: LanguageOptions; settings: object; + options: Array; + id: string; // method replacements cwd: string; filename: string; physicalFilename: string sourceCode: SourceCode; + + report(violation: Violation): void; + } ``` -**Should we eliminate `:exit` from selectors?** +### Should we eliminate `:exit` from selectors? The `:exit` was left over from before we used `esquery` and is a bit of an outlier from how selectors generally work. We could eliminate it and ask people to write their rule visitors like this: @@ -571,26 +559,45 @@ The `:exit` was left over from before we used `esquery` and is a bit of an outli } ``` +And if they just wanted to use `:exit`, we could ask them to write this: + +```js +{ + "FunctionExpression": { + exit(node, parent) { + // ... + } + } +} +``` + This would allow the visitor keys to stay strictly as selectors while still allowing both enter and exit phases of traversal to be hit. +### Does the ESLint team want to create and maintain additional languages? +The next logical step after this refactoring would be to create an additional language to ensure that other languages can be supported. JSON seems like a logical one (as many people seem to think ESLint validates JSON already). -## Help Needed +Does the ESLint team want to create an official JSON language plugin? Or any other language plugin? Or do we want to leave additional languages to the community? + +I think there is some benefit to having a few officially-supported language plugins that people will likely want, but that also brings additional maintenance burden. - +N/A ## Frequently Asked Questions -**Why allow binary files?** +### Why allow binary files? I think there may be a use in validating non-text files such as images, videos, and WebAssembly files. I may be wrong, but by allowing languages to specify that they'd prefer to receive the file in binary, it at least opens up the possibility that people may create plugins that can validate binary files in some way. -**Why not have `parse()` return a `SourceCode` instead of needing to call another method?** +### Will the JavaScript language live in the main eslint repo? + +As a first step, yes, the JavaScript language object will still live in the main eslint repo. This will allow us to iterate faster and keep an eye on backwards compatibility issues. + +In the future, I anticipate moving all JavaScript-specific functionality, including rules, into a separate repository (`eslint/js`) and publishing it as a separate package (`@eslint/js`). That would require some thought around how the website should change and be built as rule documentation would also move into a new repo. + +### Why not have `parse()` return a `SourceCode` instead of needing to call another method? It's important that we are able to determine the amount of time it takes to parse a file in order to optimize performance. Combining the parsing with creating a `SourceCode` object, which may require additional processing of the parse result, means obscuring this important number. @@ -598,9 +605,6 @@ Additionally, `createSourceCode()` allows integrators to do their own parsing an ## Related Discussions - +* [#6974 Proposal for parser services](https://github.com/eslint/eslint/issues/6974) +* [#8392 Allow parsers to supply their visitor keys](https://github.com/eslint/eslint/issues/8392) +* [#15475 Change Request: Allow async parser](https://github.com/eslint/eslint/issues/15475) From 0de3ba9d1008ca85b25c9f22adee56da128206cd Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 26 Dec 2022 12:46:24 -0800 Subject: [PATCH 06/28] Mention alternate proposal --- designs/2022-languages/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 2903184e..9dfb5fa9 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -24,6 +24,8 @@ In addition, there are numerous projects that hack around other languages to mak Instead of forcing others to rewrite what is core linting functionality, or asking people to produce an ESTree format AST for some language other than JavaScript, we can make everyone's lives easier by providing formal language support such that languages can be plugged in to ESLint easily. +We did also previously accept [#56](https://github.com/eslint/rfcs/pull/56) as an extension to `parseForESLint()` to support more languages. + ## Detailed Design This proposal consists of the following changes: @@ -514,7 +516,7 @@ This proposal does not require any changes from end-users. ## Alternatives -1. We could continue using the `parserForESLint()` approach to support other languages. +1. We could continue using the `parserForESLint()` approach to support other languages. This was proposed and accepted in [#56](https://github.com/eslint/rfcs/pull/56). 1. We could choose to not support languages other than JavaScript. ## Open Questions From 9930f1580a486a835af5eefa1d4967cf139d1cb4 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 26 Dec 2022 12:46:51 -0800 Subject: [PATCH 07/28] Add RFC link --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 9dfb5fa9..d9f522f2 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -1,6 +1,6 @@ - Repo: eslint/eslint - Start Date: 2022-11-25 -- RFC PR: (leave this empty, to be filled in later) +- RFC PR: https://github.com/eslint/rfcs/pull/99 - Authors: Nicholas C. Zakas # ESLint Language Plugins From 6de0cfa3286dcf704ff01b28bf249fe0389ad6e8 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Jan 2023 11:46:01 -0800 Subject: [PATCH 08/28] Update designs/2022-languages/README.md Co-authored-by: Brandon Mills --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index d9f522f2..468ff5b8 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -45,7 +45,7 @@ Languages can be defined in plugins by using the `languages` key and giving the ```js // inside a plugin called eslint-plugin-lang -exports default { +export default { languages: { lang1: { From 9901c0162b92b1e4161f5af29fa7e56766c636a7 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Jan 2023 11:46:10 -0800 Subject: [PATCH 09/28] Update designs/2022-languages/README.md Co-authored-by: Brandon Mills --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 468ff5b8..5ead9bfa 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -71,7 +71,7 @@ Here, `lang1` and `lang2` are languages defined by the plugin. End users can spe // inside eslint.config.js import lang from "eslint-plugin-lang"; -exports default [ +export default [ { plugins: { lang From 33bee0b33ed927cf480161d8c4695726a6bf5770 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Jan 2023 12:47:47 -0800 Subject: [PATCH 10/28] Incorporate first bits of feedback --- designs/2022-languages/README.md | 54 ++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 5ead9bfa..6365bc6d 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -94,15 +94,28 @@ Each language definition object must implement the following interface: ```ts interface ESLintLanguage { - /** - * Indicates how ESLint should read the file. - */ - fileType: "text" | "binary"; - - /** - * Validates languageOptions for this language. - */ - validateOptions(options: LanguageOptions): void; + options: { + + /** + * Indicates how ESLint should treat the file. + */ + fileType: "text" | "binary"; + + /** + * First line number returned from the parser (text mode only). + */ + lineStart?: 0 | 1; + + /** + * First column number returned from the parser (text mode only). + */ + columnStart?: 0 | 1; + + /** + * Schema for language options. + */ + languageOptionsSchema: ObjectSchemaDefinitions; + } /** * Parses the given file input into its component parts. @@ -112,16 +125,22 @@ interface ESLintLanguage { /** * Creates SourceCode object that ESLint uses to work with a file. */ - createSourceCode(file: File, input: ParseResult, env: LanguageContext): SourceCode | Promise; + createSourceCode(input: ParseResult, env: LanguageContext): SourceCode | Promise; } // Supplemental interfaces -interface LanguageOptions { - // language-defined +interface ObjectSchemaDefinition { + required?: boolean; + merge(a: unknown, b: unknown): unknown; + validate(value: unknown): void | never; } +type ObjectSchemaDefinitions = Record; + +type LanguageOptions = Record; + interface File { /** @@ -159,18 +178,19 @@ interface ParseResult { } ``` -At a high-level, ESLint uses the methods on a language object in the following way: +At a high-level, ESLint uses the language object in the following way: -* During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that * When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. +* When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers user line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. +* The `languageOptionsSchema` is an [`ObjectSchema`](https://npmjs.com/package/@humanwhocodes/object-schema) definition object that can be used to validate the langauge options. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. -#### The `validateOptions()` Method +#### The `languageOptionsSchema` Property -The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of the schema](https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/config/flat-config-schema.js#L445). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). +Right now, we have an `ObjectSchema` definition for `languageOptions` inside of [`flat-config-schema.js`](https://github.com/eslint/eslint/blob/d8c8ede088e1f4a82c9b6b5c2772af268b9161aa/lib/config/flat-config-schema.js#L445). We won't be able to use a static definition because each language define what it would like inside of its `languageOptions`. -The `validateOptions()` method must throw an error if any part of the `languageOptions` object is invalid. +With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and using `languageOptionsSchema` to create a new `ObjectSchema` object [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). No specific `languageOptions` keys will be required for languages. The `parser` and `parserOptions` keys are unique to JavaScript and may not make sense for other languages. From ce137af1be14ab3c0b664b0837068648e09b768c Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 23 Jan 2023 14:03:16 -0800 Subject: [PATCH 11/28] Flesh out SourceCode interface --- designs/2022-languages/README.md | 95 +++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 6365bc6d..3b883d26 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -24,7 +24,7 @@ In addition, there are numerous projects that hack around other languages to mak Instead of forcing others to rewrite what is core linting functionality, or asking people to produce an ESTree format AST for some language other than JavaScript, we can make everyone's lives easier by providing formal language support such that languages can be plugged in to ESLint easily. -We did also previously accept [#56](https://github.com/eslint/rfcs/pull/56) as an extension to `parseForESLint()` to support more languages. +We did also previously accept [#56](https://github.com/eslint/rfcs/pull/56) as an extension to `parseForESLint()` to support more languages and [eslint/eslint#14745](https://github.com/eslint/eslint/issues/14745) has been open for a couple of years to figure out a way to lint both a file and its virtual parts. ## Detailed Design @@ -182,7 +182,7 @@ At a high-level, ESLint uses the language object in the following way: * When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. * When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers user line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. -* The `languageOptionsSchema` is an [`ObjectSchema`](https://npmjs.com/package/@humanwhocodes/object-schema) definition object that can be used to validate the langauge options. +* The `languageOptionsSchema` is an [`ObjectSchema`](https://npmjs.com/package/@humanwhocodes/object-schema) definition object that can be used to validate the language options. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. @@ -243,6 +243,17 @@ interface SourceCode { * Return all of the inline areas where ESLint should be disabled/enabled. */ getDisableDirectives(): Array; + + /** + * Return any inline configuration provided via comments. + */ + getInlineConfig(): Array; + + /** + * Get any virtual files (blocks of other languages embedded inside of + * this file). Example: Extracting CSS and JavaScript blocks from HTML. + */ + getVirtualFiles(): Array; } type TraversalStep = VisitTraversalStep | CallTraversalStep; @@ -276,6 +287,62 @@ interface DisableDirective { loc: LocationRange; } +type VirtualFile = VirtualTextFile | VirtualBinaryFile; + +interface VirtualTextFile { + + /** + * The name of the file without a path. + */ + filename: string; + + /** + * The body of the file to parse. + */ + body: string | ArrayBuffer; + + /** + * The number to add to any line-based violations. 0 if undefined. + */ + lineStart?: number; + + /** + * The number to add to any violations that occur in the first line of + * the virtual file. 0 if undefined. + */ + columnStart?: number; + + /** + * The number to add to the column number of each violation in the body. + * 0 if undefined. + */ + indentOffset?: number; +} + +interface VirtualBinaryFile { + + /** + * The name of the file without a path. + */ + filename: string; + + /** + * The body of the file to parse. + */ + body: ArrayBuffer; + + /** + * The number to add to violation offset locations. 0 if undefined. + */ + byteStart?: number; + + /** + * The number to add to the offset location of each violation in the body. + * 0 if undefined. + */ + byteOffset?: number; +} + interface LocationRange { start: Location; end: Location; @@ -432,6 +499,26 @@ The ESLint core will then use the disable directive information to: This functionality currently lives in [`apply-disable-directives.js`](https://github.com/eslint/eslint/blob/0311d81834d675b8ae7cc92a460b37115edc4018/lib/linter/apply-disable-directives.js) and will have to be updated to call `SourceCode#getDisableDirectives()`. +This is a method instead of a property because if ESLint is run with `noInlineConfig: true` then there is no reason to calculate the disable directives. + +#### The `SoureCode#getInlineConfig()` Method + +In JavaScript, we use `/* eslint */` comments to specify inline configuration of rules. Other languages may want to do the same thing, so this method provides a way for a language to extract any inline configuration and return it to the ESLint core. + +This functionality currently lives in [`linter.js`](https://github.com/eslint/eslint/blob/ea10ca5b7b5bd8f6e6daf030ece9a3a82f10994c/lib/linter/linter.js#L466) and will have to be updated to call `SourceCode#getInlineConfig()`. + +This is a method instead of a property because if ESLint is run with `noInlineConfig: true` then there is no reason to calculate inline config. + +#### The `SoureCode#getVirtualFiles()` Method + +This method allows a file to specify that it contains "virtual" files, i.e., blocks of code written in another language. For languages such as Markdown and HTML, this would allow ESLint to lint both the primary language the file is written in and then lint each of the blocks and provide a unified report for the entire file. + +The design of `VirtualTextFile` is intended to automate the mapping of line/column reporting that currently has to be done manually inside of a processor in the `postprocess()` step. By specifying the line and column location of the virtual file inside its parent, we can calculate that automatically. + +The core would call `Source#getVirtualFiles()` and start lint processes on each of the virtual files. To each rule, virtual files would look the same as if they came from a processor. + +This could make processors obsolete going forward. + ### Extracting JavaScript Functionality An important part of this process will be extracting all of the JavaScript functionality for the ESLint core and placing it in a language object. As a first step, that language object will live in the main ESLint repository so we can easily test and make changes to ensure backwards compatibility. Eventually, though, I'd like to move this functionality into its own `eslint/js` repo. @@ -613,6 +700,10 @@ N/A I think there may be a use in validating non-text files such as images, videos, and WebAssembly files. I may be wrong, but by allowing languages to specify that they'd prefer to receive the file in binary, it at least opens up the possibility that people may create plugins that can validate binary files in some way. +### How will we autofix binary files? + +At least to start, we won't allow autofix for binary files. While it is technically possible to overwrite particular byte offsets in the same way we do character ranges, it's difficult to know how this should work in practice without an actual use case. My suggestion is that we implement linting for binary files and see what people end up using it for before designing autofix for binary files. + ### Will the JavaScript language live in the main eslint repo? As a first step, yes, the JavaScript language object will still live in the main eslint repo. This will allow us to iterate faster and keep an eye on backwards compatibility issues. From 796d2e87a1eb849991c3c38993abf9f0bbd8c4e9 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 2 Feb 2023 13:14:25 -0800 Subject: [PATCH 12/28] Add example for getVirtualFiles() --- designs/2022-languages/README.md | 122 +++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 3b883d26..78b3a6e2 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -519,6 +519,128 @@ The core would call `Source#getVirtualFiles()` and start lint processes on each This could make processors obsolete going forward. +As an example, suppose we have an HTML language plugin that can parse the following file: + +```html + + + + Example + + + + +``` + +The HTML language plugin will first parse the HTML into an AST. Inside of that AST, there will be a `Script` node representing the JavaScript portion of the file and a `Style` node representing the CSS portion. Inside the HTML AST, these two portions will be plain text. The plugin can then create two virtual files to represent those portions, such as: + +```js +class HTMLSourceCode { + + // other methods omitted + + getVirtualFiles() { + + return [ + + // the JavaScript file + { + filename: "0.js", + body: this.text.slice(70, 138), + lineStart: 6, + columnStart: 0, + indentOffset: 8 + }, + + // the CSS file + { + filename: "0.css", + body: this.text.slice(164, 219), + lineStart: 11, + columnStart: 0, + indentOffset: 8 + } + + ]; + } +} +``` + +The ESLint core will call `SourceCode#getVirtualFiles()` and add the two virtual files into the list of files to be linted. When all virtual files are linted, the ESLint core will take any violations from virtual files and combine them with the violations from the primary file to create a unified report for the physical file (in this case, the HTML file). + +Rules and languages can be targeted at the virtual files in a config file by treating them as if they were parts returned by a processor, such as: + +```js +import js from "@eslint/js"; +import html from "eslint-plugin-html"; +import css from "eslint-plugin-css"; + +export default [ + + { + plugins: { + js, + css, + html + } + }, + + // target the HTML files + { + files: ["**/*.html"], + language: "html/html", + rules: { + "html/doctype": "error" + } + }, + + // target the JS files -- include virtual files + { + files: ["**/*.js"], + language: "js/js", + rules: { + "js/semi": "error" + } + }, + + // target the CSS files -- includes virtual files + { + files: ["**/*.css"], + language: "css/css", + rules: { + "css/number-units": "error" + } + }, + + // target only virtual JS files + { + files: ["**/*.html/*.js"], + language: "js/js", + rules: { + "js/semi": "warn" + } + }, + + // target only virtual CSS files + { + files: ["**/*.html/*.css"], + language: "css/css", + rules: { + "css/number-units": "warn" + } + } +]; +``` + + ### Extracting JavaScript Functionality An important part of this process will be extracting all of the JavaScript functionality for the ESLint core and placing it in a language object. As a first step, that language object will live in the main ESLint repository so we can easily test and make changes to ensure backwards compatibility. Eventually, though, I'd like to move this functionality into its own `eslint/js` repo. From ef029b37bbbe000ec383395020b6eae192063e16 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 6 Feb 2023 13:40:10 -0800 Subject: [PATCH 13/28] Remove getVirtualFiles() --- designs/2022-languages/README.md | 193 ------------------------------- 1 file changed, 193 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 78b3a6e2..cce70217 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -248,12 +248,6 @@ interface SourceCode { * Return any inline configuration provided via comments. */ getInlineConfig(): Array; - - /** - * Get any virtual files (blocks of other languages embedded inside of - * this file). Example: Extracting CSS and JavaScript blocks from HTML. - */ - getVirtualFiles(): Array; } type TraversalStep = VisitTraversalStep | CallTraversalStep; @@ -287,62 +281,6 @@ interface DisableDirective { loc: LocationRange; } -type VirtualFile = VirtualTextFile | VirtualBinaryFile; - -interface VirtualTextFile { - - /** - * The name of the file without a path. - */ - filename: string; - - /** - * The body of the file to parse. - */ - body: string | ArrayBuffer; - - /** - * The number to add to any line-based violations. 0 if undefined. - */ - lineStart?: number; - - /** - * The number to add to any violations that occur in the first line of - * the virtual file. 0 if undefined. - */ - columnStart?: number; - - /** - * The number to add to the column number of each violation in the body. - * 0 if undefined. - */ - indentOffset?: number; -} - -interface VirtualBinaryFile { - - /** - * The name of the file without a path. - */ - filename: string; - - /** - * The body of the file to parse. - */ - body: ArrayBuffer; - - /** - * The number to add to violation offset locations. 0 if undefined. - */ - byteStart?: number; - - /** - * The number to add to the offset location of each violation in the body. - * 0 if undefined. - */ - byteOffset?: number; -} - interface LocationRange { start: Location; end: Location; @@ -509,137 +447,6 @@ This functionality currently lives in [`linter.js`](https://github.com/eslint/es This is a method instead of a property because if ESLint is run with `noInlineConfig: true` then there is no reason to calculate inline config. -#### The `SoureCode#getVirtualFiles()` Method - -This method allows a file to specify that it contains "virtual" files, i.e., blocks of code written in another language. For languages such as Markdown and HTML, this would allow ESLint to lint both the primary language the file is written in and then lint each of the blocks and provide a unified report for the entire file. - -The design of `VirtualTextFile` is intended to automate the mapping of line/column reporting that currently has to be done manually inside of a processor in the `postprocess()` step. By specifying the line and column location of the virtual file inside its parent, we can calculate that automatically. - -The core would call `Source#getVirtualFiles()` and start lint processes on each of the virtual files. To each rule, virtual files would look the same as if they came from a processor. - -This could make processors obsolete going forward. - -As an example, suppose we have an HTML language plugin that can parse the following file: - -```html - - - - Example - - - - -``` - -The HTML language plugin will first parse the HTML into an AST. Inside of that AST, there will be a `Script` node representing the JavaScript portion of the file and a `Style` node representing the CSS portion. Inside the HTML AST, these two portions will be plain text. The plugin can then create two virtual files to represent those portions, such as: - -```js -class HTMLSourceCode { - - // other methods omitted - - getVirtualFiles() { - - return [ - - // the JavaScript file - { - filename: "0.js", - body: this.text.slice(70, 138), - lineStart: 6, - columnStart: 0, - indentOffset: 8 - }, - - // the CSS file - { - filename: "0.css", - body: this.text.slice(164, 219), - lineStart: 11, - columnStart: 0, - indentOffset: 8 - } - - ]; - } -} -``` - -The ESLint core will call `SourceCode#getVirtualFiles()` and add the two virtual files into the list of files to be linted. When all virtual files are linted, the ESLint core will take any violations from virtual files and combine them with the violations from the primary file to create a unified report for the physical file (in this case, the HTML file). - -Rules and languages can be targeted at the virtual files in a config file by treating them as if they were parts returned by a processor, such as: - -```js -import js from "@eslint/js"; -import html from "eslint-plugin-html"; -import css from "eslint-plugin-css"; - -export default [ - - { - plugins: { - js, - css, - html - } - }, - - // target the HTML files - { - files: ["**/*.html"], - language: "html/html", - rules: { - "html/doctype": "error" - } - }, - - // target the JS files -- include virtual files - { - files: ["**/*.js"], - language: "js/js", - rules: { - "js/semi": "error" - } - }, - - // target the CSS files -- includes virtual files - { - files: ["**/*.css"], - language: "css/css", - rules: { - "css/number-units": "error" - } - }, - - // target only virtual JS files - { - files: ["**/*.html/*.js"], - language: "js/js", - rules: { - "js/semi": "warn" - } - }, - - // target only virtual CSS files - { - files: ["**/*.html/*.css"], - language: "css/css", - rules: { - "css/number-units": "warn" - } - } -]; -``` - ### Extracting JavaScript Functionality From b1394701895b009554a0a31a2aa459663e561a95 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:11:16 -0800 Subject: [PATCH 14/28] Revert language options schema --- designs/2022-languages/README.md | 61 +++++++++++++------------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index cce70217..80591ab2 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -94,28 +94,25 @@ Each language definition object must implement the following interface: ```ts interface ESLintLanguage { - options: { - - /** - * Indicates how ESLint should treat the file. - */ - fileType: "text" | "binary"; - - /** - * First line number returned from the parser (text mode only). - */ - lineStart?: 0 | 1; - - /** - * First column number returned from the parser (text mode only). - */ - columnStart?: 0 | 1; - - /** - * Schema for language options. - */ - languageOptionsSchema: ObjectSchemaDefinitions; - } + /** + * Indicates how ESLint should read the file. + */ + fileType: "text" | "binary"; + + /** + * First line number returned from the parser (text mode only). + */ + lineStart?: 0 | 1; + + /** + * First column number returned from the parser (text mode only). + */ + columnStart?: 0 | 1; + + /** + * Validates languageOptions for this language. + */ + validateOptions(options: LanguageOptions): void; /** * Parses the given file input into its component parts. @@ -125,20 +122,12 @@ interface ESLintLanguage { /** * Creates SourceCode object that ESLint uses to work with a file. */ - createSourceCode(input: ParseResult, env: LanguageContext): SourceCode | Promise; + createSourceCode(file: File, input: ParseResult, env: LanguageContext): SourceCode | Promise; } // Supplemental interfaces -interface ObjectSchemaDefinition { - required?: boolean; - merge(a: unknown, b: unknown): unknown; - validate(value: unknown): void | never; -} - -type ObjectSchemaDefinitions = Record; - type LanguageOptions = Record; interface File { @@ -178,19 +167,19 @@ interface ParseResult { } ``` -At a high-level, ESLint uses the language object in the following way: +At a high-level, ESLint uses the methods on a language object in the following way: +* During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that * When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. * When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers user line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. -* The `languageOptionsSchema` is an [`ObjectSchema`](https://npmjs.com/package/@humanwhocodes/object-schema) definition object that can be used to validate the language options. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. -#### The `languageOptionsSchema` Property +#### The `validateOptions()` Method -Right now, we have an `ObjectSchema` definition for `languageOptions` inside of [`flat-config-schema.js`](https://github.com/eslint/eslint/blob/d8c8ede088e1f4a82c9b6b5c2772af268b9161aa/lib/config/flat-config-schema.js#L445). We won't be able to use a static definition because each language define what it would like inside of its `languageOptions`. +The intent of this method is to validate the `languageOptions` object as specified in a config. With flat config, all of the options in `languageOptions` are related to JavaScript and are currently validated [inside of the schema](https://github.com/eslint/eslint/blob/6380c87c563be5dc78ce0ddd5c7409aaf71692bb/lib/config/flat-config-schema.js#L445). With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and calling the `validateOptions()` method [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). -With this proposal, that validation will instead need to be based on the `language` specified in the config, which will require retrieving the associated language object and using `languageOptionsSchema` to create a new `ObjectSchema` object [inside of `FlatConfigArray`](https://github.com/eslint/eslint/blob/f89403553b31d24f4fc841424cc7dcb8c3ef689f/lib/config/flat-config-array.js#L180). +The `validateOptions()` method must throw an error if any part of the `languageOptions` object is invalid. No specific `languageOptions` keys will be required for languages. The `parser` and `parserOptions` keys are unique to JavaScript and may not make sense for other languages. From ef7fea2f9a3d2da8e3dd1cd1b7c890b95d075ab3 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:26:48 -0800 Subject: [PATCH 15/28] esquery changes --- designs/2022-languages/README.md | 37 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 80591ab2..3b2f4dcb 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -109,6 +109,11 @@ interface ESLintLanguage { */ columnStart?: 0 | 1; + /** + * The property to read the node name from. Used in selector querying. + */ + nodeName: string; + /** * Validates languageOptions for this language. */ @@ -174,6 +179,7 @@ At a high-level, ESLint uses the methods on a language object in the following w * When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers user line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. +* The `nodeName` property indicates the property key in which each AST node stores the node name. Even though ESTree uses `type`, it's also common for ASTs to use `kind` to store this information. This property will allow ESLint to use ASTs as they are instead of forcing them to use `type`. This is important as it allows for querying by selector. #### The `validateOptions()` Method @@ -218,11 +224,6 @@ interface SourceCode { */ body: string | ArrayBuffer; - /** - * Determines if a node matches a given selector. - */ - match(node: ASTNode, selector: string): boolean; - /** * Traversal of AST. */ @@ -284,14 +285,6 @@ interface Location { Other than these interface members, languages may define any additional methods or properties that they need to provide to rule developers. For instance, the JavaScript `SourceCode` object currently has methods allowing retrieval of tokens, comments, and lines. It is up to the individual language object implementations to determine what additional properties and methods may be required inside of rules. (We may want to provide some best practices for other methods, but they won't be required.) -#### The `SourceCode#match()` Method - -ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match visitor patterns to nodes. `esquery` is defined for use specifically with ESTree-compatible AST structures, which means that it cannot work for other structures without modification. We need a generic way to ensure that a given pattern in a visitor matches a given node, and that is the purpose of the `SourceCode#match()` method. - -For JavaScript, `SourceCode#match()` will simply use `esquery`; for other languages, they will be able to either implement their own CSS-query utilities or else just stick with node names to get started. (ESLint initially only matched the `type` property; `esquery` was added several years later.) - -We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use `SourceCode#match()` instead of `esquery`. This will require significant refactoring. - #### The `SoureCode#traverse()` Method Today, ESLint uses a [custom traverser](https://github.com/eslint/eslint/blob/b3a08376cfb61275a7557d6d166b6116f36e5ac2/lib/shared/traverser.js) based on data from [`eslint-visitor-keys`](https://npmjs.com/package/eslint-visitor-keys). All of this is specific to an ESTree-compatible AST structure. There are two problems with this: @@ -436,7 +429,6 @@ This functionality currently lives in [`linter.js`](https://github.com/eslint/es This is a method instead of a property because if ESLint is run with `noInlineConfig: true` then there is no reason to calculate inline config. - ### Extracting JavaScript Functionality An important part of this process will be extracting all of the JavaScript functionality for the ESLint core and placing it in a language object. As a first step, that language object will live in the main ESLint repository so we can easily test and make changes to ensure backwards compatibility. Eventually, though, I'd like to move this functionality into its own `eslint/js` repo. @@ -449,6 +441,21 @@ The default flat config will need to be updated to specify a default `language` The JavaScript-specific traversal functionality that currently lives in [`linter.js`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L1140-L1158) will need to move into `SourceCode#traverse()`. +#### Generic AST selectors + +ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match visitor patterns to nodes. `esquery` is defined for use specifically with ESTree-compatible AST structures, which means that it cannot work for other structures without modification. We need a generic way to ensure that a given pattern in a visitor matches a given node, and to do that, we will need to fork `esquery`, extract the JS-specific parts while ensuring backwards compatibility with existing syntax, and then publish our own fork for use in ESLint. + +The fork will need to accept a property key that determines which property contains the AST node name (the `ESLintLanguage#nodeName` property). Some of the places that will need to be addressed: + +* [`isNode()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L270) +* [`matches() identifier`](https://github.com/estools/esquery/blob/master/esquery.js#L99) +* [`matches() class`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) (this contains the ESTree-specific functionality that should be extracted) +* [`getVisitorKeys()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L246-L261) + +The best path forward here is likely to create a new class to represent the query engine with each of the top-level functions becoming methods on that class. + +We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use our fork instead of `esquery`. This will require significant refactoring. + #### Split Disable Directive Functionality The [`applyDisableDirectives()`](https://github.com/eslint/eslint/blob/dfc7ec11b11b56daaa10e8e6d08c5cddfc8c2c59/lib/linter/linter.js#L1427-L1434) method is called inside of `Linter`, and if `Linter` calls `SourceCode#getDisableDirectives()`, we can likely reuse a lot of the existing functionality with some minor tweaks. @@ -481,7 +488,7 @@ In order to make all of this work, we'll need to make the following changes in t 1. `FlatConfigArray` will need to be updated to define the `language` key and to delegate validation of `languageOptions` to the language. 1. `Linter` will need to be updated to honor the `language` key and to use JS-specific functionality where we need to provide backwards compatibility for existing JS rules. There will be a lot of code removed and delegated to the language being used, including filtering of violation via disable directives, traversing the AST, and formulating the final violation list. 1. The `context` object passed to rules will need to be updated so it works for all languages. -1. `RuleTester` will need to be updated to support passing in `language`. We should also updated `RuleTester` to warn about the newly-deprecated `context` methods and properties. +1. `FlatRuleTester` will need to be updated to support passing in `language`. We should also updated `FlatRuleTester` to warn about the newly-deprecated `context` methods and properties. #### Updating Rule Context From c2e93380257d76443cd2f4f43bdd9f1495de002d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:28:10 -0800 Subject: [PATCH 16/28] SourceCode changes --- designs/2022-languages/README.md | 33 +++++++++----------------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 3b2f4dcb..5fdaa4f2 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -502,11 +502,19 @@ interface RuleContext { options: Array; id: string; + // method replacements + cwd: string; + filename: string; + physicalFilename: string + sourceCode: SourceCode; + + report(violation: Violation): void; + + // deprecated methods only available in JS getCwd(): string; getFilename(): string; getPhysicalFilename(): string; getSourceCode(): SourceCode; - report(violation: Violation): void; } interface Violation { @@ -553,29 +561,6 @@ This proposal does not require any changes from end-users. ## Open Questions -### Should `RuleContext` have mostly properties instead of a mix of properties and methods? - -With the proposal as-is, `RuleContext` will have four properties and five methods, but four of the five methods just return a value. If we are looking at a larger rewrite, should we create a cleaner interface with just properties? Such as: - -```ts -interface RuleContext { - - languageOptions: LanguageOptions; - settings: object; - options: Array; - id: string; - - // method replacements - cwd: string; - filename: string; - physicalFilename: string - sourceCode: SourceCode; - - report(violation: Violation): void; - -} -``` - ### Should we eliminate `:exit` from selectors? The `:exit` was left over from before we used `esquery` and is a bit of an outlier from how selectors generally work. We could eliminate it and ask people to write their rule visitors like this: From 99ae3149e33801ac208b2b04dc53e8d146db9def Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:38:19 -0800 Subject: [PATCH 17/28] Simplify violation interface --- designs/2022-languages/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 5fdaa4f2..714d2267 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -520,7 +520,8 @@ interface RuleContext { interface Violation { messageId: string; data: object; - loc: LocationRange; + start: Location; + end: Location; suggest: Array; fix(fixer: Fixer): Iterable; } From f894926ab9e62ab444cb6c8998820a511e8a8bf7 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:40:23 -0800 Subject: [PATCH 18/28] Added FAQs about rules and autofixing --- designs/2022-languages/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 714d2267..d7ceed0b 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -627,6 +627,18 @@ It's important that we are able to determine the amount of time it takes to pars Additionally, `createSourceCode()` allows integrators to do their own parsing and create their own `SourceCode` instances without requiring the use of a specific `parse()` method. +### How will rules be written for non-JS languages? + +Rules will be written the same way regardless of the language being used. The only things that will change are the AST node names and the `SourceCode` object passed in. + +### Will non-JS languages be able to autofix? + +Yes. The same text-based mechanism we currently use for JavaScript autofixes will also work for other languages. + +### What about AST mutation for autofixing? + +That is out of scope for this RFC. + ## Related Discussions * [#6974 Proposal for parser services](https://github.com/eslint/eslint/issues/6974) From 496d26f8c9b1fda4ccce8c661d7ed26f44bdc9e5 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 14 Feb 2023 12:44:04 -0800 Subject: [PATCH 19/28] FAQ update --- designs/2022-languages/README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index d7ceed0b..198c1776 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -593,14 +593,6 @@ And if they just wanted to use `:exit`, we could ask them to write this: This would allow the visitor keys to stay strictly as selectors while still allowing both enter and exit phases of traversal to be hit. -### Does the ESLint team want to create and maintain additional languages? - -The next logical step after this refactoring would be to create an additional language to ensure that other languages can be supported. JSON seems like a logical one (as many people seem to think ESLint validates JSON already). - -Does the ESLint team want to create an official JSON language plugin? Or any other language plugin? Or do we want to leave additional languages to the community? - -I think there is some benefit to having a few officially-supported language plugins that people will likely want, but that also brings additional maintenance burden. - ## Help Needed N/A @@ -639,6 +631,12 @@ Yes. The same text-based mechanism we currently use for JavaScript autofixes wil That is out of scope for this RFC. +### Will the ESLint team create and maintain additional languages? + +Yes. In the short-term, we will create a JSON language plugin and a Markdown language plugin (to coincide with or replace `eslint-plugin-markdown`). + +We may kickstart other language plugins as well and invite others to maintain them. + ## Related Discussions * [#6974 Proposal for parser services](https://github.com/eslint/eslint/issues/6974) From ab234255126b5b02e13bde0f03ffa2cf11b17e97 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 15 Feb 2023 12:47:31 -0800 Subject: [PATCH 20/28] Add matchesSelectorClass() method --- designs/2022-languages/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 198c1776..c44a9058 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -119,6 +119,14 @@ interface ESLintLanguage { */ validateOptions(options: LanguageOptions): void; + /** + * Helper for esquery that allows languages to match nodes against + * class. esquery currently has classes like `function` that will + * match all the various function nodes. This method allows languages + * to implement similar shorthands. + */ + matchesSelectorClass(className: string, node: ASTNode, ancestry: Array): boolean; + /** * Parses the given file input into its component parts. */ @@ -170,6 +178,10 @@ interface ParseResult { ast: ASTNode; body: string | ArrayBuffer; } + +interface ASTNode { + // language-defined +} ``` At a high-level, ESLint uses the methods on a language object in the following way: @@ -189,6 +201,12 @@ The `validateOptions()` method must throw an error if any part of the `languageO No specific `languageOptions` keys will be required for languages. The `parser` and `parserOptions` keys are unique to JavaScript and may not make sense for other languages. +#### The `matchesSelectorClass()` Method + +Inside of `esquery`, there are [some shortcuts](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) like `function` and `expression` that will match more than one type of node. These are all specific to ESTree and might not make sense for other languages. However, these shortcut classes are very convenient and other languages might want to implement something similar. + +As part of this effort, we will fork `esquery` to make it more generic. We will pass in the `matchSelectorClass()` method to `esquery` so it can make classes in a language-specific way. + #### The `parse()` Method The `parse()` method receives the file information and information about the ESLint context in order to correctly parse the file. This is a raw parse, meaning that `parse()` should do nothing more than convert the file into an abstract format without doing any postprocessing. This is important so that ESLint can tell how long parsing takes vs. other operations. (One problem with the current `parseForESLint()` approach is that we lose visibility into the true parsing time.) @@ -449,7 +467,7 @@ The fork will need to accept a property key that determines which property conta * [`isNode()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L270) * [`matches() identifier`](https://github.com/estools/esquery/blob/master/esquery.js#L99) -* [`matches() class`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) (this contains the ESTree-specific functionality that should be extracted) +* [`matches() class`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) (this contains the ESTree-specific functionality that should be extracted to `ESLintLanguage#matchesSelectorClass()`) * [`getVisitorKeys()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L246-L261) The best path forward here is likely to create a new class to represent the query engine with each of the top-level functions becoming methods on that class. @@ -621,7 +639,7 @@ Additionally, `createSourceCode()` allows integrators to do their own parsing an ### How will rules be written for non-JS languages? -Rules will be written the same way regardless of the language being used. The only things that will change are the AST node names and the `SourceCode` object passed in. +Rules will be written the same way regardless of the language being used. The only things that will change are the AST node names and the `SourceCode` object passed in. Otherwise, rules can be written the same way, including the use of CSS-like selector strings to find nodes. ### Will non-JS languages be able to autofix? From 70020672cd690bafad7f0e4f93905c1f66b412ba Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 16 Feb 2023 12:22:59 -0800 Subject: [PATCH 21/28] Update designs/2022-languages/README.md Co-authored-by: James Henry --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index c44a9058..3eb72886 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -188,7 +188,7 @@ At a high-level, ESLint uses the methods on a language object in the following w * During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that * When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. -* When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers user line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. +* When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers use line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. * The `nodeName` property indicates the property key in which each AST node stores the node name. Even though ESTree uses `type`, it's also common for ASTs to use `kind` to store this information. This property will allow ESLint to use ASTs as they are instead of forcing them to use `type`. This is important as it allows for querying by selector. From 5c3b554f6a8ee9c2182cbe2c3791b82e724ac2e1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 21 Feb 2023 10:07:50 -0800 Subject: [PATCH 22/28] Update ParseResult definition --- designs/2022-languages/README.md | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 3eb72886..61c32217 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -128,7 +128,9 @@ interface ESLintLanguage { matchesSelectorClass(className: string, node: ASTNode, ancestry: Array): boolean; /** - * Parses the given file input into its component parts. + * Parses the given file input into its component parts. This file should not + * throws errors for parsing errors but rather should return any parsing + * errors as parse of the ParseResult object. */ parse(file: File, env: LanguageContext): ParseResult | Promise; @@ -175,8 +177,37 @@ interface LanguageContext { } interface ParseResult { + + /** + * Indicates if the parse was successful. If true, the parse was successful + * and ESLint should continue on to create a SourceCode object and run rules; + * if false, ESLint should just report the error(s) without doing anything + * else. + */ + ok: boolean; + + /** + * The abstract syntax tree created by the parser. + */ ast: ASTNode; + + /** + * The body of the file, either text or bytes. + */ body: string | ArrayBuffer; + + /** + * Any parsing errors, whether fatal or not. + */ + errors: Array; +} + +interface ParseError { + message: string; + line: number; + column: number; + endLine: number; + endColumn: number; } interface ASTNode { From ab092116ce98a0f08450cb283c8f5e686538e0ae Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 21 Feb 2023 10:09:04 -0800 Subject: [PATCH 23/28] Clean up errors and violations types --- designs/2022-languages/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 61c32217..7a948aac 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -206,8 +206,8 @@ interface ParseError { message: string; line: number; column: number; - endLine: number; - endColumn: number; + endLine?: number; + endColumn?: number; } interface ASTNode { @@ -569,8 +569,10 @@ interface RuleContext { interface Violation { messageId: string; data: object; - start: Location; - end: Location; + line: number; + column: number; + endLine?: number; + endColumn?: number suggest: Array; fix(fixer: Fixer): Iterable; } From f98e7eb974e6b17be8f604839512c8f4bdcdf3ea Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 21 Feb 2023 10:15:26 -0800 Subject: [PATCH 24/28] Update method signatures --- designs/2022-languages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 7a948aac..1cfb6f7b 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -132,12 +132,12 @@ interface ESLintLanguage { * throws errors for parsing errors but rather should return any parsing * errors as parse of the ParseResult object. */ - parse(file: File, env: LanguageContext): ParseResult | Promise; + parse(file: File, context: LanguageContext): ParseResult | Promise; /** * Creates SourceCode object that ESLint uses to work with a file. */ - createSourceCode(file: File, input: ParseResult, env: LanguageContext): SourceCode | Promise; + createSourceCode(file: File, input: ParseResult, context: LanguageContext): SourceCode | Promise; } From c6fcdb37c31c6b3ae98b6e2c54c80a20b53b14d1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 24 Feb 2023 11:33:14 -0800 Subject: [PATCH 25/28] Make ParseResult#errors optional --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 1cfb6f7b..13589a34 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -199,7 +199,7 @@ interface ParseResult { /** * Any parsing errors, whether fatal or not. */ - errors: Array; + errors?: Array; } interface ParseError { From 07be8916cbd49e3c4e8386de8365e48ed318550b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 1 Mar 2023 13:55:16 -0800 Subject: [PATCH 26/28] Update based on esquery PRs --- designs/2022-languages/README.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 13589a34..6b368e6e 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -110,9 +110,9 @@ interface ESLintLanguage { columnStart?: 0 | 1; /** - * The property to read the node name from. Used in selector querying. + * The property to read the node type from. Used in selector querying. */ - nodeName: string; + nodeTypeKey: string; /** * Validates languageOptions for this language. @@ -222,7 +222,7 @@ At a high-level, ESLint uses the methods on a language object in the following w * When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers use line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result. * The raw parse result is then passed into `createSourceCode()` to create the `SourceCode` object that ESLint will use to interact with the code. -* The `nodeName` property indicates the property key in which each AST node stores the node name. Even though ESTree uses `type`, it's also common for ASTs to use `kind` to store this information. This property will allow ESLint to use ASTs as they are instead of forcing them to use `type`. This is important as it allows for querying by selector. +* The `nodeTypeKey` property indicates the property key in which each AST node stores the node name. Even though ESTree uses `type`, it's also common for ASTs to use `kind` to store this information. This property will allow ESLint to use ASTs as they are instead of forcing them to use `type`. This is important as it allows for querying by selector. #### The `validateOptions()` Method @@ -236,7 +236,7 @@ No specific `languageOptions` keys will be required for languages. The `parser` Inside of `esquery`, there are [some shortcuts](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) like `function` and `expression` that will match more than one type of node. These are all specific to ESTree and might not make sense for other languages. However, these shortcut classes are very convenient and other languages might want to implement something similar. -As part of this effort, we will fork `esquery` to make it more generic. We will pass in the `matchSelectorClass()` method to `esquery` so it can make classes in a language-specific way. +The `esquery` [External class resolve](https://github.com/estools/esquery/pull/140) pull request implements an option that allows us to pass in a function to interpret pseudoclasses from `ESLintLanguage#matchesSelectorClass()`. #### The `parse()` Method @@ -492,18 +492,12 @@ The JavaScript-specific traversal functionality that currently lives in [`linter #### Generic AST selectors -ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match visitor patterns to nodes. `esquery` is defined for use specifically with ESTree-compatible AST structures, which means that it cannot work for other structures without modification. We need a generic way to ensure that a given pattern in a visitor matches a given node, and to do that, we will need to fork `esquery`, extract the JS-specific parts while ensuring backwards compatibility with existing syntax, and then publish our own fork for use in ESLint. +ESLint currently uses [`esquery`](https://npmjs.com/package/esquery) to match visitor patterns to nodes. `esquery` is defined for use specifically with ESTree-compatible AST structures, which means that it cannot work for other structures without modification. -The fork will need to accept a property key that determines which property contains the AST node name (the `ESLintLanguage#nodeName` property). Some of the places that will need to be addressed: +To make `esquery` more generic, I've submitted the following pull requests: -* [`isNode()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L270) -* [`matches() identifier`](https://github.com/estools/esquery/blob/master/esquery.js#L99) -* [`matches() class`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L213-L233) (this contains the ESTree-specific functionality that should be extracted to `ESLintLanguage#matchesSelectorClass()`) -* [`getVisitorKeys()`](https://github.com/estools/esquery/blob/7c3800a4b2ff5c7b3eb3b2cf742865b7c908981f/esquery.js#L246-L261) - -The best path forward here is likely to create a new class to represent the query engine with each of the top-level functions becoming methods on that class. - -We will need to update the [`NodeEventGenerator`](https://github.com/eslint/eslint/blob/90a5b6b4aeff7343783f85418c683f2c9901ab07/lib/linter/node-event-generator.js#L296) to use our fork instead of `esquery`. This will require significant refactoring. +* [Allow for custom node type keys](https://github.com/estools/esquery/pull/139) that implements an option to specify which key in an AST node contains the node type. +* [External class resolve](https://github.com/estools/esquery/pull/140) implements an option that allows us to pass in a function to interpret pseudoclasses like `:expression` from `ESLintLanguage#matchesSelectorClass()`. #### Split Disable Directive Functionality From 6b1b0aaad71a4ef4819e2a776f17558140511d7b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 15 Mar 2023 11:51:41 -0700 Subject: [PATCH 27/28] Add visitorKeys --- designs/2022-languages/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index 6b368e6e..aa4f48bf 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -114,6 +114,11 @@ interface ESLintLanguage { */ nodeTypeKey: string; + /** + * The traversal path that tools should take when evaluating the AST + */ + visitorKeys: Record>; + /** * Validates languageOptions for this language. */ @@ -273,6 +278,11 @@ interface SourceCode { */ body: string | ArrayBuffer; + /** + * The traversal path that tools should take when evaluating the AST + */ + visitorKeys?: Record>; + /** * Traversal of AST. */ @@ -334,6 +344,10 @@ interface Location { Other than these interface members, languages may define any additional methods or properties that they need to provide to rule developers. For instance, the JavaScript `SourceCode` object currently has methods allowing retrieval of tokens, comments, and lines. It is up to the individual language object implementations to determine what additional properties and methods may be required inside of rules. (We may want to provide some best practices for other methods, but they won't be required.) +#### The `SoureCode#visitorKeys` Property + +The JavaScript language allows a `parser` option to be passed in, and so the result AST may not be the exact structure represented on the `ESLintLanguage` object. In such a case, an additional `visitorKeys` property can be provided on `SourceCode` that overrides the `ESLintLanguage#visitorKeys` property just for this file. + #### The `SoureCode#traverse()` Method Today, ESLint uses a [custom traverser](https://github.com/eslint/eslint/blob/b3a08376cfb61275a7557d6d166b6116f36e5ac2/lib/shared/traverser.js) based on data from [`eslint-visitor-keys`](https://npmjs.com/package/eslint-visitor-keys). All of this is specific to an ESTree-compatible AST structure. There are two problems with this: From 2131c0943b32eb2e8cee78017e8d92a71ab00b2e Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 16 Mar 2023 14:14:56 -0700 Subject: [PATCH 28/28] Finish sentence --- designs/2022-languages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2022-languages/README.md b/designs/2022-languages/README.md index aa4f48bf..3078b03f 100644 --- a/designs/2022-languages/README.md +++ b/designs/2022-languages/README.md @@ -222,7 +222,7 @@ interface ASTNode { At a high-level, ESLint uses the methods on a language object in the following way: -* During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that +* During config validation, ESLint calls `validateOptions()` on the `languageOptions` specified in the config. It is the expectation that this method throws an error if any of the options are invalid. * When ESLint reads a file, it checks `fileType` to determine whether the language would prefer the file in text or binary format. * When preparing violation messages, ESLint uses `lineStart` and `columnStart` to determine how to offset the locations. Some parsers use line 0 as the first line but ESLint normalizes to line 1 as the first line (similar for columns). Using `lineStart` and `columnStart` allows ESLint to ensure a consistently reported output so one file doesn't start with line 0 and another starts at line 1. If not present, `lineStart` and `columnStart` are assumed to be 0. * After reading a file, ESLint passes the file information to `parse()` to create a raw parse result.