From 65873ba277d5f40c5a82db46c981ac7470cd7d6b Mon Sep 17 00:00:00 2001 From: overlookmotel <557937+overlookmotel@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:32:03 +0000 Subject: [PATCH] refactor(linter/plugins): add stubs for all `SourceCode` methods (#14285) Add stub implementations for all methods and properties of `SourceCode`. All unimplemented methods/getters just throw an error. --- apps/oxlint/src-js/index.ts | 27 +- apps/oxlint/src-js/plugins/fix.ts | 5 +- apps/oxlint/src-js/plugins/scope.ts | 84 ++++ apps/oxlint/src-js/plugins/source_code.ts | 452 +++++++++++++++++++++- apps/oxlint/src-js/plugins/types.ts | 36 +- 5 files changed, 593 insertions(+), 11 deletions(-) create mode 100644 apps/oxlint/src-js/plugins/scope.ts diff --git a/apps/oxlint/src-js/index.ts b/apps/oxlint/src-js/index.ts index 91f9f722534fa..e2a625bc2bb43 100644 --- a/apps/oxlint/src-js/index.ts +++ b/apps/oxlint/src-js/index.ts @@ -3,10 +3,31 @@ import type { CreateOnceRule, Plugin, Rule } from './plugins/load.ts'; import type { BeforeHook, Visitor, VisitorWithHooks } from './plugins/types.ts'; export type { Context, Diagnostic } from './plugins/context.ts'; -export type { Fix, Fixer, FixFn, NodeOrToken, Range } from './plugins/fix.ts'; +export type { Fix, Fixer, FixFn, Range } from './plugins/fix.ts'; export type { CreateOnceRule, CreateRule, Plugin, Rule } from './plugins/load.ts'; -export type { SourceCode } from './plugins/source_code.ts'; -export type { AfterHook, BeforeHook, Node, RuleMeta, Visitor, VisitorWithHooks } from './plugins/types.ts'; +export type { + Definition, + DefinitionType, + Reference, + Scope, + ScopeManager, + ScopeType, + Variable, +} from './plugins/scope.ts'; +export type { CountOptions, FilterFn, RangeOptions, SkipOptions, SourceCode } from './plugins/source_code.ts'; +export type { + AfterHook, + BeforeHook, + Comment, + LineColumn, + Location, + Node, + NodeOrToken, + RuleMeta, + Token, + Visitor, + VisitorWithHooks, +} from './plugins/types.ts'; const { defineProperty, getPrototypeOf, hasOwn, setPrototypeOf, create: ObjectCreate } = Object; diff --git a/apps/oxlint/src-js/plugins/fix.ts b/apps/oxlint/src-js/plugins/fix.ts index 0b7ba3566123b..bdded6355cf8d 100644 --- a/apps/oxlint/src-js/plugins/fix.ts +++ b/apps/oxlint/src-js/plugins/fix.ts @@ -1,7 +1,7 @@ import { assertIs } from './utils.js'; import type { Diagnostic, InternalContext } from './context.ts'; -import type { Node } from './types.ts'; +import type { NodeOrToken } from './types.ts'; const { prototype: ArrayPrototype, from: ArrayFrom } = Array, { getPrototypeOf, hasOwn, prototype: ObjectPrototype } = Object, @@ -20,9 +20,6 @@ export type Fix = { range: Range; text: string }; export type Range = [number, number]; -// Currently we only support `Node`s, but will add support for `Token`s later -export type NodeOrToken = Node; - // Fixer, passed as argument to `fix` function passed to `Context#report()`. // // Fixer is stateless, so reuse a single object for all fixes. diff --git a/apps/oxlint/src-js/plugins/scope.ts b/apps/oxlint/src-js/plugins/scope.ts new file mode 100644 index 0000000000000..c7caaf4b14b9c --- /dev/null +++ b/apps/oxlint/src-js/plugins/scope.ts @@ -0,0 +1,84 @@ +import type * as ESTree from '@oxc-project/types'; + +import type { Node } from './types.ts'; + +type Identifier = + | ESTree.IdentifierName + | ESTree.IdentifierReference + | ESTree.BindingIdentifier + | ESTree.LabelIdentifier + | ESTree.TSThisParameter + | ESTree.TSIndexSignatureName; + +export class ScopeManager { + // TODO +} + +export interface Scope { + type: ScopeType; + isStrict: boolean; + upper: Scope | null; + childScopes: Scope[]; + variableScope: Scope; + block: Node; + variables: Variable[]; + set: Map; + references: Reference[]; + through: Reference[]; + functionExpressionScope: boolean; + implicit?: { + variables: Variable[]; + set: Map; + }; +} + +export type ScopeType = + | 'block' + | 'catch' + | 'class' + | 'class-field-initializer' + | 'class-static-block' + | 'for' + | 'function' + | 'function-expression-name' + | 'global' + | 'module' + | 'switch' + | 'with'; + +export interface Variable { + name: string; + scope: Scope; + identifiers: Identifier[]; + references: Reference[]; + defs: Definition[]; +} + +export interface Reference { + identifier: Identifier; + from: Scope; + resolved: Variable | null; + writeExpr: ESTree.Expression | null; + init: boolean; + isWrite(): boolean; + isRead(): boolean; + isReadOnly(): boolean; + isWriteOnly(): boolean; + isReadWrite(): boolean; +} + +export interface Definition { + type: DefinitionType; + name: Identifier; + node: Node; + parent: Node | null; +} + +export type DefinitionType = + | 'CatchClause' + | 'ClassName' + | 'FunctionName' + | 'ImplicitGlobalVariable' + | 'ImportBinding' + | 'Parameter' + | 'Variable'; diff --git a/apps/oxlint/src-js/plugins/source_code.ts b/apps/oxlint/src-js/plugins/source_code.ts index 68f0c7ff05885..2ec8db349a21a 100644 --- a/apps/oxlint/src-js/plugins/source_code.ts +++ b/apps/oxlint/src-js/plugins/source_code.ts @@ -1,4 +1,7 @@ -import type { Node } from './types.ts'; +import type { Program } from '@oxc-project/types'; + +import type { Scope, ScopeManager, Variable } from './scope.ts'; +import type { Comment, LineColumn, Node, NodeOrToken, Token } from './types.ts'; const { max } = Math; @@ -15,6 +18,42 @@ export class SourceCode { // Initially `null`, but set to source text string before linting each file. text: string = null as unknown as string; + // `true` if source text has Unicode BOM. + // TODO: Set this correctly + hasBOM: boolean = false; + + // Get AST of the file. + get ast(): Program { + throw new Error('`sourceCode.ast` not implemented yet'); // TODO + } + + // Get `ScopeManager` for the file. + get scopeManager(): ScopeManager { + throw new Error('`sourceCode.scopeManager` not implemented yet'); // TODO + } + + // Get visitor keys to traverse this AST. + get visitorKeys(): { [key: string]: string[] } { + throw new Error('`sourceCode.visitorKeys` not implemented yet'); // TODO + } + + // Get parser services for the file. + get parserServices(): { [key: string]: unknown } { + throw new Error('`sourceCode.parserServices` not implemented yet'); // TODO + } + + // Get source text as array of lines, split according to specification's definition of line breaks. + get lines(): string[] { + throw new Error('`sourceCode.lines` not implemented yet'); // TODO + } + + /** + * Get the source code for the given node. + * @param node? - The AST node to get the text for. + * @param beforeCount? - The number of characters before the node to retrieve. + * @param afterCount? - The number of characters after the node to retrieve. + * @returns Source text representing the AST node. + */ getText( node?: Node | null | undefined, beforeCount?: number | null | undefined, @@ -30,5 +69,414 @@ export class SourceCode { return this.text.slice(start, end); } - // TODO: Add more methods + /** + * Retrieve an array containing all comments in the source code. + * @returns Array of `Comment`s in occurrence order. + */ + getAllComments(): Comment[] { + throw new Error('`sourceCode.getAllComments` not implemented yet'); // TODO + } + + /** + * Get all comment tokens directly before the given node or token. + * @param nodeOrToken - The AST node or token to check for adjacent comment tokens. + * @returns Array of `Comment`s in occurrence order. + */ + // oxlint-disable-next-line no-unused-vars + getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] { + throw new Error('`sourceCode.getCommentsBefore` not implemented yet'); // TODO + } + + /** + * Get all comment tokens directly after the given node or token. + * @param nodeOrToken - The AST node or token to check for adjacent comment tokens. + * @returns Array of `Comment`s in occurrence order. + */ + // oxlint-disable-next-line no-unused-vars + getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] { + throw new Error('`sourceCode.getCommentsAfter` not implemented yet'); // TODO + } + + /** + * Get all comment tokens inside the given node. + * @param node - The AST node to get the comments for. + * @returns Array of `Comment`s in occurrence order. + */ + // oxlint-disable-next-line no-unused-vars + getCommentsInside(node: Node): Comment[] { + throw new Error('`sourceCode.getCommentsInside` not implemented yet'); // TODO + } + + /** + * Determine if two nodes or tokens have at least one whitespace character between them. + * Order does not matter. Returns `false` if the given nodes or tokens overlap. + * @param nodeOrToken1 - The first node or token to check between. + * @param nodeOrToken2 - The second node or token to check between. + * @returns `true` if there is a whitespace character between + * any of the tokens found between the two given nodes or tokens. + */ + // oxlint-disable-next-line no-unused-vars + isSpaceBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean { + throw new Error('`sourceCode.isSpaceBetween` not implemented yet'); // TODO + } + + /** + * Determine whether the given identifier node is a reference to a global variable. + * @param node - `Identifier` node to check. + * @returns `true` if the identifier is a reference to a global variable. + */ + // oxlint-disable-next-line no-unused-vars + isGlobalReference(node: Node): boolean { + throw new Error('`sourceCode.isGlobalReference` not implemented yet'); // TODO + } + + /** + * Get all tokens that are related to the given node. + * @param node - The AST node. + * @param countOptions - Options object. If this is a function then it's `options.filter`. + * @returns Array of `Token`s. + */ + /** + * Get all tokens that are related to the given node. + * @param node - The AST node. + * @param beforeCount? - The number of tokens before the node to retrieve. + * @param afterCount? - The number of tokens after the node to retrieve. + * @returns Array of `Token`s. + */ + /* oxlint-disable no-unused-vars */ + getTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null | undefined): Token[]; + getTokens(node: Node, beforeCount?: number | null | undefined, afterCount?: number | null | undefined): Token[] { + throw new Error('`sourceCode.getTokens` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the first token of the given node. + * @param node - The AST node. + * @param skipOptions? - Options object. If this is a number then it's `options.skip`. + * If this is a function then it's `options.filter`. + * @returns `Token`, or `null` if all were skipped. + */ + // oxlint-disable-next-line no-unused-vars + getFirstToken(node: Node, skipOptions?: SkipOptions | number | FilterFn | null | undefined): Token | null { + throw new Error('`sourceCode.getFirstToken` not implemented yet'); // TODO + } + + /** + * Get the first tokens of the given node. + * @param node - The AST node. + * @param countOptions? - Options object. If this is a number then it's `options.count`. + * If this is a function then it's `options.filter`. + * @returns Array of `Token`s. + */ + // oxlint-disable-next-line no-unused-vars + getFirstTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null | undefined): Token[] { + throw new Error('`sourceCode.getFirstTokens` not implemented yet'); // TODO + } + + /** + * Get the last token of the given node. + * @param node - The AST node. + * @param skipOptions? - Options object. Same options as `getFirstToken()`. + * @returns `Token`, or `null` if all were skipped. + */ + // oxlint-disable-next-line no-unused-vars + getLastToken(node: Node, skipOptions?: SkipOptions | number | FilterFn | null | undefined): Token | null { + throw new Error('`sourceCode.getLastToken` not implemented yet'); // TODO + } + + /** + * Get the last tokens of the given node. + * @param node - The AST node. + * @param countOptions? - Options object. Same options as `getFirstTokens()`. + * @returns Array of `Token`s. + */ + // oxlint-disable-next-line no-unused-vars + getLastTokens(node: Node, countOptions?: CountOptions | number | FilterFn | null | undefined): Token[] { + throw new Error('`sourceCode.getLastTokens` not implemented yet'); // TODO + } + + /** + * Get the token that precedes a given node or token. + * @param nodeOrToken - The AST node or token. + * @param skipOptions? - Options object. Same options as `getFirstToken()`. + * @returns `Token`, or `null` if all were skipped. + */ + /* oxlint-disable no-unused-vars */ + getTokenBefore( + nodeOrToken: NodeOrToken | Comment, + skipOptions?: SkipOptions | number | FilterFn | null | undefined, + ): Token | null { + throw new Error('`sourceCode.getTokenBefore` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the tokens that precedes a given node or token. + * @param nodeOrToken - The AST node or token. + * @param countOptions? - Options object. Same options as `getFirstTokens()`. + * @returns Array of `Token`s. + */ + /* oxlint-disable no-unused-vars */ + getTokensBefore( + nodeOrToken: NodeOrToken | Comment, + countOptions?: CountOptions | number | FilterFn | null | undefined, + ): Token[] { + throw new Error('`sourceCode.getTokensBefore` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the token that follows a given node or token. + * @param nodeOrToken - The AST node or token. + * @param skipOptions? - Options object. Same options as `getFirstToken()`. + * @returns `Token`, or `null` if all were skipped. + */ + /* oxlint-disable no-unused-vars */ + getTokenAfter( + nodeOrToken: NodeOrToken | Comment, + skipOptions?: SkipOptions | number | FilterFn | null | undefined, + ): Token | null { + throw new Error('`sourceCode.getTokenAfter` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the tokens that follow a given node or token. + * @param nodeOrToken - The AST node or token. + * @param countOptions? - Options object. Same options as `getFirstTokens()`. + * @returns Array of `Token`s. + */ + /* oxlint-disable no-unused-vars */ + getTokensAfter( + nodeOrToken: NodeOrToken | Comment, + countOptions?: CountOptions | number | FilterFn | null | undefined, + ): Token[] { + throw new Error('`sourceCode.getTokensAfter` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get all of the tokens between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param countOptions? - Options object. If this is a function then it's `options.filter`. + * @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`. + */ + /** + * Get all of the tokens between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param padding - Number of extra tokens on either side of center. + * @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`. + */ + /* oxlint-disable no-unused-vars */ + getTokensBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + countOptions?: CountOptions | number | FilterFn | null | undefined, + ): Token[]; + getTokensBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + padding?: number, + ): Token[] { + throw new Error('`sourceCode.getTokensBetween` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the first token between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param countOptions? - Options object. Same options as `getFirstToken()`. + * @returns `Token`, or `null` if all were skipped. + */ + /* oxlint-disable no-unused-vars */ + getFirstTokenBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + skipOptions?: SkipOptions | null | undefined, + ): Token | null { + throw new Error('`sourceCode.getFirstTokenBetween` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the first tokens between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param countOptions? - Options object. Same options as `getFirstTokens()`. + * @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`. + */ + /* oxlint-disable no-unused-vars */ + getFirstTokensBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + countOptions?: CountOptions | number | FilterFn | null | undefined, + ): Token[] { + throw new Error('`sourceCode.getFirstTokensBetween` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the last token between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param skipOptions? - Options object. Same options as `getFirstToken()`. + * @returns `Token`, or `null` if all were skipped. + */ + /* oxlint-disable no-unused-vars */ + getLastTokenBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + skipOptions?: SkipOptions | null | undefined, + ): Token | null { + throw new Error('`sourceCode.getLastTokenBetween` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the last tokens between two non-overlapping nodes. + * @param nodeOrToken1 - Node before the desired token range. + * @param nodeOrToken2 - Node after the desired token range. + * @param countOptions? - Options object. Same options as `getFirstTokens()`. + * @returns Array of `Token`s between `nodeOrToken1` and `nodeOrToken2`. + */ + /* oxlint-disable no-unused-vars */ + getLastTokensBetween( + nodeOrToken1: NodeOrToken | Comment, + nodeOrToken2: NodeOrToken | Comment, + countOptions?: CountOptions | number | FilterFn | null | undefined, + ): Token[] { + throw new Error('`sourceCode.getLastTokensBetween` not implemented yet'); // TODO + } + /* oxlint-enable no-unused-vars */ + + /** + * Get the token starting at the specified index. + * @param index - Index of the start of the token's range. + * @param options - Options object. + * @returns The token starting at index, or `null` if no such token. + */ + // oxlint-disable-next-line no-unused-vars + getTokenByRangeStart(index: number, rangeOptions?: RangeOptions | null | undefined): Token | null { + throw new Error('`sourceCode.getTokenByRangeStart` not implemented yet'); // TODO + } + + /** + * Get the deepest node containing a range index. + * @param index Range index of the desired node. + * @returns The node if found, or `null` if not found. + */ + // oxlint-disable-next-line no-unused-vars + getNodeByRangeIndex(index: number): Node | null { + throw new Error('`sourceCode.getNodeByRangeIndex` not implemented yet'); // TODO + } + + /** + * Convert a source text index into a (line, column) pair. + * @param index The index of a character in a file. + * @returns `{line, column}` location object with 1-indexed line and 0-indexed column. + * @throws {TypeError|RangeError} If non-numeric `index`, or `index` out of range. + */ + // oxlint-disable-next-line no-unused-vars + getLocFromIndex(index: number): LineColumn { + throw new Error('`sourceCode.getLocFromIndex` not implemented yet'); // TODO + } + + /** + * Convert a `{ line, column }` pair into a range index. + * @param loc - A line/column location. + * @returns The range index of the location in the file. + * @throws {TypeError|RangeError} If `loc` is not an object with a numeric `line` and `column`, + * or if the `line` is less than or equal to zero, or the line or column is out of the expected range. + */ + // oxlint-disable-next-line no-unused-vars + getIndexFromLoc(loc: LineColumn): number { + throw new Error('`sourceCode.getIndexFromLoc` not implemented yet'); // TODO + } + + /** + * Check whether any comments exist or not between the given 2 nodes. + * @param nodeOrToken1 - The node to check. + * @param nodeOrToken2 - The node to check. + * @returns `true` if one or more comments exist. + */ + // oxlint-disable-next-line no-unused-vars + commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean { + throw new Error('`sourceCode.commentsExistBetween` not implemented yet'); // TODO + } + + /** + * Get all the ancestors of a given node. + * @param node - AST node + * @returns All the ancestor nodes in the AST, not including the provided node, + * starting from the root node at index 0 and going inwards to the parent node. + */ + // oxlint-disable-next-line no-unused-vars + getAncestors(node: Node): Node[] { + throw new Error('`sourceCode.getAncestors` not implemented yet'); // TODO + } + + /** + * Get the variables that `node` defines. + * This is a convenience method that passes through to the same method on the `scopeManager`. + * @param node - The node for which the variables are obtained. + * @returns An array of variable nodes representing the variables that `node` defines. + */ + // oxlint-disable-next-line no-unused-vars + getDeclaredVariables(node: Node): Variable[] { + throw new Error('`sourceCode.getDeclaredVariables` not implemented yet'); // TODO + } + + /** + * Get the scope for the given node + * @param node - The node to get the scope of. + * @returns The scope information for this node. + */ + // oxlint-disable-next-line no-unused-vars + getScope(node: Node): Scope { + throw new Error('`sourceCode.getScope` not implemented yet'); // TODO + } + + /** + * Mark a variable as used in the current scope + * @param name - The name of the variable to mark as used. + * @param refNode? - The closest node to the variable reference. + * @returns `true` if the variable was found and marked as used, `false` if not. + */ + // oxlint-disable-next-line no-unused-vars + markVariableAsUsed(name: string, refNode: Node): boolean { + throw new Error('`sourceCode.markVariableAsUsed` not implemented yet'); // TODO + } } + +// Options for various `SourceCode` methods e.g. `getFirstToken`. +export interface SkipOptions { + // Number of skipping tokens + skip?: number; + // `true` to include comment tokens in the result + includeComments?: boolean; + // Function to filter tokens + filter?: FilterFn | null; +} + +// Options for various `SourceCode` methods e.g. `getFirstTokens`. +export interface CountOptions { + // Maximum number of tokens to return + count?: number; + // `true` to include comment tokens in the result + includeComments?: boolean; + // Function to filter tokens + filter?: FilterFn | null; +} + +// Options for various `SourceCode` methods e.g. `getTokenByRangeStart`. +export interface RangeOptions { + // `true` to include comment tokens in the result + includeComments?: boolean; +} + +// Filter function, passed as `filter` property of `SkipOptions` and `CountOptions`. +export type FilterFn = (token: Token) => boolean; diff --git a/apps/oxlint/src-js/plugins/types.ts b/apps/oxlint/src-js/plugins/types.ts index 3a27d06c760ec..0e64d62e4df38 100644 --- a/apps/oxlint/src-js/plugins/types.ts +++ b/apps/oxlint/src-js/plugins/types.ts @@ -25,12 +25,44 @@ export interface VisitorWithHooks extends Visitor { // Visit function for a specific AST node type. export type VisitFn = (node: Node) => void; -// AST node type. -export interface Node { +// Internal interface for any type which has `start` and `end` properties. +// We'll add `range` and `loc` properties to this later. +interface Spanned { start: number; end: number; } +// AST node type. +export interface Node extends Spanned {} + +// AST token type. +export interface Token extends Spanned { + type: string; + value: string; +} + +// Currently we only support `Node`s, but will add support for `Token`s later. +export type NodeOrToken = Node | Token; + +// Comment. +export interface Comment extends Spanned { + type: 'Line' | 'Block'; + value: string; +} + +// Source code location. +export interface Location { + start: LineColumn; + end: LineColumn; +} + +// Line number + column number pair. +// `line` is 1-indexed, `column` is 0-indexed. +export interface LineColumn { + line: number; + column: number; +} + // Element of compiled visitor array. // * `VisitFn | null` for leaf nodes. // * `EnterExit | null` for non-leaf nodes.