Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions apps/oxlint/src-js/plugins/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ast, initAst, initSourceText, sourceText } from "./source_code.ts";
import visitorKeys from "../generated/keys.ts";
import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts";

import type { NodeOrToken } from "./types.ts";
import type { NodeOrToken, Node, Comment } from "./types.ts";
import type { Node as ESTreeNode } from "../generated/types.d.ts";

/**
Expand Down Expand Up @@ -254,52 +254,62 @@ export function getRange(nodeOrToken: NodeOrToken): Range {
* @param nodeOrToken - Node or token to get the location of
* @returns Location of the node or token
*/
// Note: We cannot expose `getNodeLoc` as public method, because it always recalculates the location,
// and returns a new object each time. It would be misleading if `node.loc !== sourceCode.getLoc(node)`.
// Both AST nodes and tokens handle lazy `loc` computation and caching via their respective getters
// (AST nodes via `NodeProto` prototype getter which caches via `Object.defineProperty`,
// tokens via `Token` class getter which caches in a private field).
// So accessing `.loc` gives the right behavior for both, including stable object identity.
export function getLoc(nodeOrToken: NodeOrToken): Location {
// If location is already calculated for this node or token, return it
if (Object.hasOwn(nodeOrToken, "loc")) return nodeOrToken.loc;
// Calculate location
return getNodeLoc(nodeOrToken);
return nodeOrToken.loc;
}

/**
* Calculate the `Location` for an AST node or token.
* Calculate the `Location` for an AST node or comment, and cache it on the node.
*
* Used in `loc` getters on AST nodes.
* Used in `loc` getters on AST nodes and comments (not tokens - tokens use their own caching via `Token` class).
*
* Defines a `loc` property on the node/token with the calculated `Location`, so accessing `loc` twice on same node
* Defines a `loc` property on the node/comment with the calculated `Location`, so accessing `loc` twice on same node
* results in the same object each time.
*
* For internal use only.
*
* @param nodeOrToken - AST node or token
* @param nodeOrComment - AST node or comment
* @returns Location
*/
export function getNodeLoc(nodeOrToken: NodeOrToken): Location {
// Build `lines` and `lineStartIndices` tables if they haven't been already.
// This also decodes `sourceText` if it wasn't already.
if (lines.length === 0) initLines();
export function getNodeLoc(nodeOrComment: Node | Comment): Location {
const loc = computeLoc(nodeOrComment.start, nodeOrComment.end);

const loc = {
start: getLineColumnFromOffsetUnchecked(nodeOrToken.start),
end: getLineColumnFromOffsetUnchecked(nodeOrToken.end),
};

// Define `loc` property with the calculated `Location`, so accessing `loc` twice on same node/token
// Define `loc` property with the calculated `Location`, so accessing `loc` twice on same node
// results in the same object each time.
//
// We do not make the `loc` property enumerable, because it wasn't present before.
// It would be weird if `Object.keys(node)` included `loc` if the property had been accessed previously,
// but not if it hadn't.
//
// The property is configurable so that it can be deleted when token objects are reused across files
// (see token pooling in `tokens.ts`). Deleting the own property restores the prototype getter.
Object.defineProperty(nodeOrToken, "loc", { value: loc, writable: true, configurable: true });
// We also don't make it configurable, because deleting it wouldn't make `node.loc` evaluate to `undefined`,
// because the access would fall through to the getter on the prototype.
Object.defineProperty(nodeOrComment, "loc", { value: loc, writable: true });

return loc;
}

/**
* Compute a `Location` from `start` and `end` source offsets.
*
* Pure computation - does not cache the result.
* Initializes `lines` and `lineStartIndices` tables if they haven't been already.
*
* @param start - Start offset
* @param end - End offset
* @returns Location
*/
export function computeLoc(start: number, end: number): Location {
if (lines.length === 0) initLines();
return {
start: getLineColumnFromOffsetUnchecked(start),
end: getLineColumnFromOffsetUnchecked(end),
};
}

/**
* Get the deepest node containing a range index.
* @param offset - Range index of the desired node
Expand Down
85 changes: 53 additions & 32 deletions apps/oxlint/src-js/plugins/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
*/

import { ast, buffer, initAst, initSourceText, sourceText } from "./source_code.ts";
import { getNodeLoc } from "./location.ts";
import { computeLoc } from "./location.ts";
import { TOKENS_OFFSET_POS_32, TOKENS_LEN_POS_32 } from "../generated/constants.ts";
import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.ts";

import type { Comment, Node, NodeOrToken } from "./types.ts";
import type { Span } from "./location.ts";
import type { Location, Span } from "./location.ts";

/**
* Options for various `SourceCode` methods e.g. `getFirstToken`.
Expand Down Expand Up @@ -50,7 +50,7 @@ export type FilterFn = (token: TokenOrComment) => boolean;
/**
* AST token type.
*/
export type Token =
type TokenType =
| BooleanToken
| IdentifierToken
| JSXIdentifierToken
Expand All @@ -64,6 +64,9 @@ export type Token =
| StringToken
| TemplateToken;

// Export type as `Token` for external consumers
export type { TokenType as Token };

interface BaseToken extends Span {
value: string;
regex: undefined;
Expand Down Expand Up @@ -122,27 +125,15 @@ export interface TemplateToken extends BaseToken {
type: "Template";
}

type TokenOrComment = Token | Comment;
type TokenOrComment = TokenType | Comment;

// `SkipOptions` object used by `getTokenOrCommentBefore` and `getTokenOrCommentAfter`.
// This object is reused over and over to avoid creating a new options object on each call.
const INCLUDE_COMMENTS_SKIP_OPTIONS: SkipOptions = { includeComments: true, skip: 0 };

// Prototype for `Token` objects, which calculates `loc` property lazily.
const TokenProto = Object.create(Object.prototype, {
loc: {
// Note: Not configurable
get() {
tokensWithLoc.push(this);
return getNodeLoc(this);
},
enumerable: true,
},
});

// Tokens for the current file.
// Created lazily only when needed.
export let tokens: Token[] | null = null;
export let tokens: TokenType[] | null = null;
let comments: Comment[] | null = null;
export let tokensAndComments: TokenOrComment[] | null = null;

Expand All @@ -166,14 +157,53 @@ const regexObjects: RegularExpressionToken["regex"][] = [];
// for the next regex descriptor object which can be reused.
const tokensWithRegex: Token[] = [];

// Reset `#loc` field on a `Token` class instance
let resetLoc: (token: Token) => void;

/**
* Token implementation with lazy `loc` caching via private field.
*
* Using a class with a private `#loc` field avoids hidden class transitions that would occur
* with `Object.defineProperty` / `delete` on plain objects.
* All `Token` instances always have the same V8 hidden class, keeping property access monomorphic.
*/
class Token {
type: TokenType["type"] = "" as TokenType["type"]; // Overwritten later
value: string = "";
regex: RegularExpressionToken["regex"] | undefined;
start: number = 0;
end: number = 0;
range: [number, number] = [0, 0];

#loc: Location | null = null;

get loc(): Location {
const loc = this.#loc;
if (loc !== null) return loc;

tokensWithLoc.push(this);
return (this.#loc = computeLoc(this.start, this.end));
}

static {
// Defined in static block to avoid exposing this as a public method
resetLoc = (token: Token) => {
token.#loc = null;
};
}
}

// Make `loc` property enumerable so that `for (const key in token) ...` includes `loc` in the keys it iterates over
Object.defineProperty(Token.prototype, "loc", { enumerable: true });

let uint32: Uint32Array | null = null;

// `ESTreeKind` discriminants (set by Rust side)
const PRIVATE_IDENTIFIER_KIND = 2;
const REGEXP_KIND = 8;

// Indexed by `ESTreeKind` discriminant (matches `ESTreeKind` enum in `estree_kind.rs`)
const TOKEN_TYPES: Token["type"][] = [
const TOKEN_TYPES: TokenType["type"][] = [
"Identifier",
"Keyword",
"PrivateIdentifier",
Expand Down Expand Up @@ -210,16 +240,7 @@ export function initTokens() {

// Grow cache if needed (one-time cost as cache warms up)
while (cachedTokens.length < tokensLen) {
cachedTokens.push({
// @ts-expect-error - TS doesn't understand `__proto__`
__proto__: TokenProto,
type: "" as Token["type"], // Overwritten later
value: "",
regex: undefined,
start: 0,
end: 0,
range: [0, 0],
});
cachedTokens.push(new Token());
}

// Deserialize into cached token objects
Expand All @@ -237,9 +258,9 @@ export function initTokens() {
// Assuming random distribution of file sizes, this cheaper branch should be hit on 50% of files.
if (previousTokens.length >= tokensLen) {
previousTokens.length = tokensLen;
tokens = previousTokens;
tokens = previousTokens as TokenType[];
} else {
tokens = previousTokens = cachedTokens.slice(0, tokensLen);
tokens = (previousTokens = cachedTokens.slice(0, tokensLen)) as TokenType[];
}

uint32 = null;
Expand Down Expand Up @@ -461,12 +482,12 @@ function debugCheckTokensAndComments() {
/**
* Reset tokens after file has been linted.
*
* Deletes cached `loc` from tokens that had it accessed, so the prototype getter
* Clears cached `loc` on tokens that had it accessed, so the getter
* will recalculate it when the token is reused for a different file.
*/
export function resetTokens() {
for (let i = 0, len = tokensWithLoc.length; i < len; i++) {
delete (tokensWithLoc[i] as { loc?: unknown }).loc;
resetLoc(tokensWithLoc[i]);
}
tokensWithLoc.length = 0;

Expand Down
Loading
Loading