Skip to content
Open
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
85 changes: 47 additions & 38 deletions packages/vinext/src/plugins/ast-scope.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
collectBindingNames,
forEachAstChild,
isAstRecord,
nodeArray,
type AstRecord,
} from "./ast-utils.js";
import type { ESTree } from "vite";
import { collectBindingNames, forEachAstChild } from "./ast-utils.js";

export type AstScope = {
parent: AstScope | null;
Expand All @@ -22,7 +17,9 @@ export function hasAstBinding(scope: AstScope, name: string): boolean {
return false;
}

export function isFunctionNode(node: AstRecord): boolean {
export function isFunctionNode(
node: ESTree.Node,
): node is ESTree.Function | ESTree.ArrowFunctionExpression {
return (
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression" ||
Expand All @@ -31,25 +28,34 @@ export function isFunctionNode(node: AstRecord): boolean {
}

export function collectDirectScopeBindings(
node: AstRecord,
node: ESTree.Node,
scope: AstScope,
onVariableDeclarator?: (declaration: AstRecord, declarator: AstRecord) => void,
onVariableDeclarator?: (
declaration: ESTree.VariableDeclaration,
declarator: ESTree.VariableDeclarator,
) => void,
): void {
for (const statementValue of nodeArray(node.body)) {
const statement = isAstRecord(statementValue) ? statementValue : null;
if (!statement) continue;
const statements =
node.type === "Program" ||
node.type === "BlockStatement" ||
node.type === "StaticBlock" ||
node.type === "TSModuleBlock"
? node.body
: node.type === "SwitchCase"
? node.consequent
: [];

for (const statement of statements) {
const declaration =
statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
? isAstRecord(statement.declaration)
? statement.declaration
: null
? statement.declaration
: statement;
if (!declaration) continue;

if (declaration.type === "ImportDeclaration") {
if (declaration.importKind === "type") continue;
for (const specifier of nodeArray(declaration.specifiers)) {
if (isAstRecord(specifier) && specifier.importKind !== "type") {
for (const specifier of declaration.specifiers) {
if (specifier.type !== "ImportSpecifier" || specifier.importKind !== "type") {
collectBindingNames(specifier.local, scope.bindings);
}
}
Expand All @@ -59,8 +65,7 @@ export function collectDirectScopeBindings(
) {
collectBindingNames(declaration.id, scope.bindings);
} else if (declaration.type === "VariableDeclaration" && declaration.declare !== true) {
for (const declarator of nodeArray(declaration.declarations)) {
if (!isAstRecord(declarator)) continue;
for (const declarator of declaration.declarations) {
collectBindingNames(declarator.id, scope.bindings);
onVariableDeclarator?.(declaration, declarator);
}
Expand All @@ -79,45 +84,49 @@ export function collectDirectScopeBindings(
}

export function collectLoopScopeBindings(
node: AstRecord,
node: ESTree.ForStatement | ESTree.ForInStatement | ESTree.ForOfStatement,
scope: AstScope,
onVariableDeclarator?: (declaration: AstRecord, declarator: AstRecord) => void,
onVariableDeclarator?: (
declaration: ESTree.VariableDeclaration,
declarator: ESTree.VariableDeclarator,
) => void,
): void {
const declarationValue = node.type === "ForStatement" ? node.init : node.left;
if (!isAstRecord(declarationValue)) return;
if (declarationValue.type !== "VariableDeclaration" || declarationValue.declare === true) return;
for (const declarator of nodeArray(declarationValue.declarations)) {
if (!isAstRecord(declarator)) continue;
if (
!declarationValue ||
declarationValue.type !== "VariableDeclaration" ||
declarationValue.declare === true
)
return;
for (const declarator of declarationValue.declarations) {
collectBindingNames(declarator.id, scope.bindings);
onVariableDeclarator?.(declarationValue, declarator);
}
}

export function collectSwitchScopeBindings(
node: AstRecord,
node: ESTree.SwitchStatement,
scope: AstScope,
onVariableDeclarator?: (declaration: AstRecord, declarator: AstRecord) => void,
onVariableDeclarator?: (
declaration: ESTree.VariableDeclaration,
declarator: ESTree.VariableDeclarator,
) => void,
): void {
for (const caseValue of nodeArray(node.cases)) {
if (!isAstRecord(caseValue)) continue;
collectDirectScopeBindings(
{ type: "BlockStatement", body: nodeArray(caseValue.consequent) },
scope,
onVariableDeclarator,
);
for (const switchCase of node.cases) {
collectDirectScopeBindings(switchCase, scope, onVariableDeclarator);
}
}

export function collectVarScopeBindings(node: AstRecord, scope: AstScope, root = true): void {
export function collectVarScopeBindings(node: ESTree.Node, scope: AstScope, root = true): void {
if (
!root &&
(isFunctionNode(node) || node.type === "StaticBlock" || node.type === "TSModuleBlock")
) {
return;
}
if (node.type === "VariableDeclaration" && node.kind === "var" && node.declare !== true) {
for (const declarator of nodeArray(node.declarations)) {
if (isAstRecord(declarator)) collectBindingNames(declarator.id, scope.bindings);
for (const declarator of node.declarations) {
collectBindingNames(declarator.id, scope.bindings);
}
}
forEachAstChild(node, (child) => collectVarScopeBindings(child, scope, false));
Expand Down
140 changes: 49 additions & 91 deletions packages/vinext/src/plugins/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
export type AstRecord = {
type: string;
start?: number;
end?: number;
[key: string]: unknown;
};

export type AstRange = AstRecord & {
start: number;
end: number;
};
import type { ESTree } from "vite";

export type ScriptParserLanguage = "js" | "jsx" | "ts" | "tsx";

const SCRIPT_MODULE_EXTENSION_RE = /^\.(?:[cm]?[jt]s|[jt]sx)$/i;
export const SCRIPT_MODULE_ID_RE = /\.(?:[cm]?[jt]s|[jt]sx)(?:[?#].*)?$/i;
const TRANSPARENT_EXPRESSION_TYPES = new Set([
"ChainExpression",
"ParenthesizedExpression",
"TSAsExpression",
"TSInstantiationExpression",
"TSNonNullExpression",
"TSSatisfiesExpression",
"TSTypeAssertion",
]);

/**
* Cheap pre-parse gate for plugins that only transform *dynamic* `import(...)`.
*
Expand Down Expand Up @@ -75,61 +55,43 @@ export function scriptParserLanguage(id: string): ScriptParserLanguage | null {

const SKIP_CHILD_KEYS = new Set(["type", "parent", "loc", "start", "end"]);

function getObjectProperty(value: unknown, key: string): unknown {
if (typeof value !== "object" || value === null) return null;
return Reflect.get(value, key);
}

export function isAstRecord(value: unknown): value is AstRecord {
return typeof getObjectProperty(value, "type") === "string";
}

function toAstRecord(value: unknown): AstRecord | null {
return isAstRecord(value) ? value : null;
}

export function nodeArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}

export function hasRange(node: AstRecord | null): node is AstRange {
return node !== null && typeof node.start === "number" && typeof node.end === "number";
}

export function isIdentifierNamed(value: unknown, name: string): boolean {
return isAstRecord(value) && value.type === "Identifier" && value.name === name;
export function isIdentifierNamed(value: ESTree.Node | null | undefined, name: string): boolean {
return value?.type === "Identifier" && value.name === name;
}

export function getAstName(value: unknown): string | null {
const node = toAstRecord(value);
if (!node) return null;
if (node.type === "Identifier" && typeof node.name === "string") return node.name;
if (typeof node.value === "string") return node.value;
export function getAstName(node: ESTree.Node | null | undefined): string | null {
if (node?.type === "Identifier") return node.name;
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
return null;
}

/** Remove syntax-only wrappers while preserving the underlying expression. */
export function unwrapExpression(value: unknown): AstRecord | null {
const node = toAstRecord(value);
if (!node || !TRANSPARENT_EXPRESSION_TYPES.has(node.type)) return node;
return unwrapExpression(node.expression);
export function unwrapExpression(node: ESTree.Node | null | undefined): ESTree.Node | null {
if (!node) return null;
if (
node.type === "ChainExpression" ||
node.type === "ParenthesizedExpression" ||
node.type === "TSAsExpression" ||
node.type === "TSInstantiationExpression" ||
node.type === "TSNonNullExpression" ||
node.type === "TSSatisfiesExpression" ||
node.type === "TSTypeAssertion"
) {
return unwrapExpression(node.expression);
}
return node;
}

/** Return the value of a string literal node, without evaluating expressions. */
export function stringLiteralValue(value: unknown): string | null {
const node = toAstRecord(value);
if (
(node?.type === "Literal" || node?.type === "StringLiteral") &&
typeof node.value === "string"
) {
export function stringLiteralValue(node: ESTree.Node | null | undefined): string | null {
if (node?.type === "Literal" && typeof node.value === "string") {
return node.value;
}
return null;
}

/** Return the value of a boolean literal node, without evaluating expressions. */
export function booleanLiteralValue(value: unknown): boolean | null {
const node = toAstRecord(value);
export function booleanLiteralValue(node: ESTree.Node | null | undefined): boolean | null {
return node?.type === "Literal" && typeof node.value === "boolean" ? node.value : null;
}

Expand All @@ -138,39 +100,32 @@ export function booleanLiteralValue(value: unknown): boolean | null {
* template literal. This deliberately does not fold concatenations or other
* expressions.
*/
export function staticStringValue(value: unknown): string | null {
const node = toAstRecord(value);
export function staticStringValue(node: ESTree.Node | null | undefined): string | null {
if (!node) return null;

const literal = stringLiteralValue(node);
if (literal !== null) return literal;
if (node.type !== "TemplateLiteral" || nodeArray(node.expressions).length !== 0) return null;
if (node.type !== "TemplateLiteral" || node.expressions.length !== 0) return null;

const quasis = nodeArray(node.quasis);
if (quasis.length !== 1) return null;
const quasi = toAstRecord(quasis[0]);
if (quasi?.type !== "TemplateElement" || typeof quasi.value !== "object" || !quasi.value) {
return null;
}
const quasi = node.quasis[0];
if (!quasi || node.quasis.length !== 1) return null;

const cooked = Reflect.get(quasi.value, "cooked");
const raw = Reflect.get(quasi.value, "raw");
const { cooked, raw } = quasi.value;
return typeof cooked === "string" ? cooked : typeof raw === "string" ? raw : null;
}

export function forEachAstChild(node: AstRecord, callback: (child: AstRecord) => void): void {
export function forEachAstChild(node: ESTree.Node, callback: (child: ESTree.Node) => void): void {
for (const [key, value] of Object.entries(node)) {
if (SKIP_CHILD_KEYS.has(key)) continue;
const child = toAstRecord(value);
if (child) {
callback(child);
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
const itemNode = toAstRecord(item);
if (itemNode) callback(itemNode);
if (typeof item === "object" && item !== null && "type" in item) {
callback(item as ESTree.Node);
}
}
} else if (typeof value === "object" && value !== null && "type" in value) {
// Object.entries() erases the discriminated-union type of Node fields.
callback(value as ESTree.Node);
}
}
}
Expand All @@ -181,19 +136,22 @@ export function forEachAstChild(node: AstRecord, callback: (child: AstRecord) =>
* expression. Parent links and source-location metadata are skipped by
* {@link forEachAstChild}, so OXC's cyclic `parent` references are safe.
*/
export function walkAst(value: unknown, visitor: (node: AstRecord) => boolean | void): void {
const node = toAstRecord(value);
if (!node || visitor(node) === false) return;
export function walkAst(node: ESTree.Node, visitor: (node: ESTree.Node) => boolean | void): void {
if (visitor(node) === false) return;
forEachAstChild(node, (child) => walkAst(child, visitor));
}

export function collectBindingNames(pattern: unknown, target: Set<string>): void {
const node = toAstRecord(pattern);
export function collectBindingNames(
node: ESTree.Node | null | undefined,
target: Set<string>,
): void {
if (!node) return;

// Binding patterns are a deliberately small subset of the full node union.
// oxlint-disable-next-line typescript/switch-exhaustiveness-check
switch (node.type) {
case "Identifier":
if (typeof node.name === "string") target.add(node.name);
target.add(node.name);
return;
case "RestElement":
collectBindingNames(node.argument, target);
Expand All @@ -205,20 +163,20 @@ export function collectBindingNames(pattern: unknown, target: Set<string>): void
collectBindingNames(node.parameter, target);
return;
case "ArrayPattern":
for (const element of nodeArray(node.elements)) collectBindingNames(element, target);
for (const element of node.elements) collectBindingNames(element, target);
return;
case "ObjectPattern":
for (const property of nodeArray(node.properties)) {
const propertyNode = toAstRecord(property);
if (!propertyNode) continue;
for (const property of node.properties) {
collectBindingNames(
propertyNode.type === "Property" ? propertyNode.value : propertyNode.argument,
property.type === "Property" ? property.value : property.argument,
target,
);
}
return;
case "Property":
collectBindingNames(node.value, target);
return;
default:
return;
}
}
Loading
Loading