perf(javascript): analyze member chains without recursive copying (#932)
* fix(javascript): traverse deep semantic member chains iteratively * fix(javascript): preserve deep initializer and pattern analysis
This commit is contained in:
1 parent
e6ba9c3af3
commit
dcde958450
7 files changed
+287
-116
No files matched your search
@@ -157,13 +157,12 @@ const nativeRequire = (
|
||||
):
|
||||
| { readonly specifier: string; readonly member: string | null }
|
||||
| undefined => {
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const nested = t.isNode(node.object)
|
||||
? nativeRequire(node.object)
|
||||
: undefined;
|
||||
if (nested === undefined) return undefined;
|
||||
const member = semanticStaticPropertyName(node.property, node.computed);
|
||||
return { ...nested, member: member === "" ? nested.member : member };
|
||||
let member: string | null = null;
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
if (!t.isNode(node.object)) return undefined;
|
||||
const property = semanticStaticPropertyName(node.property, node.computed);
|
||||
if (member === null && property !== "") member = property;
|
||||
node = node.object;
|
||||
}
|
||||
if (!t.isCallExpression(node)) return undefined;
|
||||
const name = calleeName(node.callee);
|
||||
@@ -175,7 +174,7 @@ const nativeRequire = (
|
||||
return undefined;
|
||||
const specifier = stringValue(argumentNode(node.arguments[0]));
|
||||
return specifier !== undefined && isNativeSpecifier(specifier, "require")
|
||||
? { specifier, member: null }
|
||||
? { specifier, member }
|
||||
: undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import * as t from "@babel/types";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { onlyBinding, origin } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import { calleeName } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
const memberChain = ".next".repeat(12_000);
|
||||
|
||||
it("retains native addon observations through a deep member initializer", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
`const value = require("./addon.node")${memberChain}.last[""];`,
|
||||
);
|
||||
expect(analysis.electron.native_addon_bindings).toEqual([
|
||||
expect.objectContaining({ specifier: "./addon.node", members: ["last"] }),
|
||||
]);
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps a deep non-native initializer outside native addon observations", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(
|
||||
`const value = root${memberChain}.last;`,
|
||||
);
|
||||
expect(analysis.electron.native_addon_bindings).toEqual([]);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains unknown value diagnostics and references after a deep member initializer", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`const value = root${memberChain}.last;`,
|
||||
);
|
||||
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
|
||||
expect(onlyBinding(ir, "value")).toMatchObject({
|
||||
value: { status: "unknown", reason: "Cannot project last from unknown." },
|
||||
provenance: { status: "unknown", reason: "Unbound identifier root." },
|
||||
});
|
||||
expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([
|
||||
{ name: "root", role: "read" },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`const value = require("./dependency.js")${memberChain}[""].last;`,
|
||||
`const dependency = require("./dependency.js"); const value = dependency${memberChain}[""].last;`,
|
||||
`const value = require("./dependency.js")?.next${memberChain}[""].last;`,
|
||||
])("preserves the full literal module path (case %#)", (source) => {
|
||||
const ir = analyzeJavaScriptSemantics(source);
|
||||
const path = origin(onlyBinding(ir, "value"));
|
||||
expect(path.specifier).toBe("./dependency.js");
|
||||
expect(path.importedPath).toEqual([
|
||||
...(source.includes("?.next") ? ["next"] : []),
|
||||
...Array.from({ length: 12_000 }, () => "next"),
|
||||
"",
|
||||
"last",
|
||||
]);
|
||||
expect(ir.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains a deep CommonJS re-export path without inventing dynamic module links", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
module.exports.result = require("./dependency.js")${memberChain}.last;
|
||||
const dynamic = require("./other.js")${memberChain}[key].last;
|
||||
`);
|
||||
expect(ir.moduleLinks).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "commonjs-export",
|
||||
specifier: "./dependency.js",
|
||||
importedName: "last",
|
||||
exportedName: "result",
|
||||
}),
|
||||
]);
|
||||
expect(onlyBinding(ir, "dynamic").provenance).toMatchObject({
|
||||
status: "unknown",
|
||||
reason: "Dynamic provenance member.",
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates a deep mutation while retaining unrelated literal properties", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
const root = { untouched: "retained" };
|
||||
root${memberChain}.last = 1;
|
||||
const value = root.untouched;
|
||||
`);
|
||||
expect(onlyBinding(ir, "root").value).toEqual({
|
||||
status: "object",
|
||||
properties: [
|
||||
{ name: "untouched", value: { status: "literal", value: "retained" } },
|
||||
],
|
||||
unknownProperties: true,
|
||||
omittedProperties: null,
|
||||
});
|
||||
expect(onlyBinding(ir, "value").value).toEqual({
|
||||
status: "literal",
|
||||
value: "retained",
|
||||
});
|
||||
});
|
||||
|
||||
it("retains the complete location and unresolved target of a deep call", () => {
|
||||
const callee = `root${memberChain}.last`;
|
||||
const ir = analyzeJavaScriptSemantics(`${callee}();`);
|
||||
expect(ir.callSites).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "call",
|
||||
resolution: "unresolved",
|
||||
calleeCallableIds: [],
|
||||
calleeLocation: {
|
||||
start: { line: 1, column: 0 },
|
||||
end: { line: 1, column: callee.length },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains deep default values without confusing their reads with parameter bindings", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`export function inspect(value = root${memberChain}.last) { return value; }`,
|
||||
);
|
||||
expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([
|
||||
{ name: "root", role: "read" },
|
||||
{ name: "value", role: "read" },
|
||||
]);
|
||||
expect(ir.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("keeps a shadowed require local through a deep member initializer", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`function inspect(require) { const value = require("./dependency.js")${memberChain}.last; return value; }`,
|
||||
);
|
||||
expect(ir.moduleLinks).toEqual([]);
|
||||
expect(onlyBinding(ir, "value").provenance.origins).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["root.first.last()", "root.first.last"],
|
||||
['root[""].last()', "root..last"],
|
||||
['("")[""].last()', "last"],
|
||||
['("")[""]()', ""],
|
||||
["root[0].last()", "root.0.last"],
|
||||
["root[key].last()", "root.[computed@5].last"],
|
||||
["root?.first.last()", "root.first.last"],
|
||||
[`root${memberChain}.last()`, `root${memberChain}.last`],
|
||||
])("preserves exact callee syntax (case %#)", (source, expected) => {
|
||||
const file = parseJavaScriptSource(source);
|
||||
const statement = file?.program.body[0];
|
||||
if (
|
||||
!t.isExpressionStatement(statement) ||
|
||||
(!t.isCallExpression(statement.expression) &&
|
||||
!t.isOptionalCallExpression(statement.expression))
|
||||
)
|
||||
throw new Error("Expected parsed call expression");
|
||||
expect(calleeName(statement.expression.callee)).toBe(expected);
|
||||
});
|
||||
@@ -39,11 +39,18 @@ export const collectSemanticMemberMutations = (
|
||||
return;
|
||||
}
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
markValue(
|
||||
node.object,
|
||||
[semanticStaticPropertyKey(node.property, node.computed), ...path],
|
||||
bindings,
|
||||
);
|
||||
const members: (string | null)[] = [];
|
||||
let current: t.Node = node;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
members.push(
|
||||
semanticStaticPropertyKey(current.property, current.computed),
|
||||
);
|
||||
current = current.object;
|
||||
}
|
||||
markValue(current, [...members.reverse(), ...path], bindings);
|
||||
return;
|
||||
}
|
||||
for (const value of referencedValues(node, path))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type {
|
||||
JavaScriptModuleOrigin,
|
||||
JavaScriptSemanticBinding,
|
||||
JavaScriptSemanticModuleLink,
|
||||
JavaScriptSemanticReference,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticScopeState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import {
|
||||
@@ -100,12 +100,16 @@ export const collectSemanticReferences = (
|
||||
): JavaScriptSemanticReference[] => {
|
||||
const output: JavaScriptSemanticReference[] = [];
|
||||
const seen = new Set<string>();
|
||||
const patterns: t.Node[] = [];
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (isPatternNode(node)) patterns.push(node);
|
||||
if (
|
||||
!t.isIdentifier(node) ||
|
||||
parent === null ||
|
||||
(name !== undefined && node.name !== name)
|
||||
(name !== undefined && node.name !== name) ||
|
||||
(isNonReferenceKey(node, parent) &&
|
||||
!patterns.some((pattern) => bindsIdentifier(pattern, parent, node)))
|
||||
)
|
||||
return;
|
||||
const role = semanticIdentifierRole(node, parent, readAncestors());
|
||||
@@ -138,6 +142,9 @@ export const collectSemanticReferences = (
|
||||
});
|
||||
}
|
||||
},
|
||||
exit: (node) => {
|
||||
if (isPatternNode(node)) patterns.pop();
|
||||
},
|
||||
});
|
||||
return output;
|
||||
};
|
||||
@@ -174,40 +181,6 @@ export const immutableSemanticBindings = (
|
||||
}))
|
||||
.sort((left, right) => compareCodePoints(left.bindingId, right.bindingId));
|
||||
|
||||
/** Recover a literal require origin plus an optional member chain. */
|
||||
const semanticModuleOrigin = (
|
||||
node: t.Node | null | undefined,
|
||||
projection: readonly (string | number | null)[],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const nested = t.isNode(node.object)
|
||||
? semanticModuleOrigin(node.object, projection, state)
|
||||
: undefined;
|
||||
const member = semanticStaticPropertyKey(node.property, node.computed);
|
||||
return nested === undefined || member === null
|
||||
? undefined
|
||||
: { ...nested, importedPath: [...nested.importedPath, member] };
|
||||
}
|
||||
if (
|
||||
!t.isCallExpression(node) ||
|
||||
!t.isIdentifier(node.callee, { name: "require" })
|
||||
)
|
||||
return undefined;
|
||||
if (
|
||||
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
|
||||
semanticResolutionBlocked(state, node.callee, "require")
|
||||
)
|
||||
return undefined;
|
||||
const specifier = stringValue(node.arguments[0]);
|
||||
return specifier === undefined || projection.includes(null)
|
||||
? undefined
|
||||
: {
|
||||
specifier,
|
||||
importedPath: projection.map((segment) => String(segment)),
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
semanticStaticPropertyKey,
|
||||
semanticStaticPropertyName,
|
||||
@@ -318,7 +291,7 @@ const collectRequireLink = (
|
||||
node: t.VariableDeclarator,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): void => {
|
||||
const origin = semanticModuleOrigin(node.init, [], state);
|
||||
const origin = semanticRequireOrigin(node.init, state);
|
||||
if (origin === undefined) return;
|
||||
for (const binding of requirePatternBindings(node.id, origin.importedPath))
|
||||
addModuleLink(state, {
|
||||
@@ -364,7 +337,7 @@ const collectCommonJsExport = (
|
||||
): void => {
|
||||
const exportedName = commonJsExportName(node.left, state);
|
||||
if (exportedName === undefined) return;
|
||||
const origin = semanticModuleOrigin(node.right, [], state);
|
||||
const origin = semanticRequireOrigin(node.right, state);
|
||||
addModuleLink(state, {
|
||||
kind: "commonjs-export",
|
||||
specifier: origin?.specifier ?? null,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Recover an unshadowed literal require origin and its exact member path. */
|
||||
export const semanticRequireOrigin = (
|
||||
node: t.Node | null | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
const members: string[] = [];
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const member = semanticStaticPropertyKey(node.property, node.computed);
|
||||
if (member === null || !t.isNode(node.object)) return undefined;
|
||||
members.push(member);
|
||||
node = node.object;
|
||||
}
|
||||
if (
|
||||
!t.isCallExpression(node) ||
|
||||
!t.isIdentifier(node.callee, { name: "require" })
|
||||
)
|
||||
return undefined;
|
||||
if (
|
||||
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
|
||||
semanticResolutionBlocked(state, node.callee, "require")
|
||||
)
|
||||
return undefined;
|
||||
const specifier = stringValue(node.arguments[0]);
|
||||
return specifier === undefined
|
||||
? undefined
|
||||
: { specifier, importedPath: members.reverse() };
|
||||
};
|
||||
@@ -4,17 +4,15 @@ import { invalidateSemanticMutationPath } from "./javascriptSemanticMutationValu
|
||||
|
||||
import type {
|
||||
JavaScriptBindingProvenance,
|
||||
JavaScriptModuleOrigin,
|
||||
JavaScriptSemanticProperty,
|
||||
JavaScriptSemanticValue,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
|
||||
import {
|
||||
@@ -253,15 +251,28 @@ const evaluateMember = (
|
||||
node: t.MemberExpression | t.OptionalMemberExpression,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptSemanticValue => {
|
||||
if (!t.isNode(node.object))
|
||||
return { status: "unknown", reason: "Unsupported member base." };
|
||||
const key =
|
||||
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
|
||||
if (key === undefined)
|
||||
return { status: "unknown", reason: "Dynamic member key." };
|
||||
return projectValue(evaluateExpression(node.object, nestedContext(context)), [
|
||||
key,
|
||||
]);
|
||||
const keys: string[] = [];
|
||||
let current: t.Node = node;
|
||||
let value: JavaScriptSemanticValue | undefined;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
if (!t.isNode(current.object)) {
|
||||
value = { status: "unknown", reason: "Unsupported member base." };
|
||||
break;
|
||||
}
|
||||
const key = semanticStaticPropertyKey(current.property, current.computed);
|
||||
if (key === null) {
|
||||
value = { status: "unknown", reason: "Dynamic member key." };
|
||||
break;
|
||||
}
|
||||
keys.push(key);
|
||||
current = current.object;
|
||||
}
|
||||
value ??= evaluateExpression(current, nestedContext(context));
|
||||
for (const key of keys.reverse()) value = projectValue(value, [key]);
|
||||
return value;
|
||||
};
|
||||
|
||||
const evaluateAddition = (
|
||||
@@ -425,7 +436,7 @@ const provenanceForExpression = (
|
||||
node: t.Node,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptBindingProvenance => {
|
||||
const required = requireOrigin(node, context.state);
|
||||
const required = semanticRequireOrigin(node, context.state);
|
||||
if (required !== undefined) return semanticOriginsProvenance([required]);
|
||||
if (t.isIdentifier(node)) {
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
@@ -437,25 +448,37 @@ const provenanceForExpression = (
|
||||
: provenanceForBinding(binding, nestedContext(context));
|
||||
}
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
if (!t.isNode(node.object))
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
"Unsupported member base.",
|
||||
const members: string[] = [];
|
||||
let current: t.Node = node;
|
||||
while (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
if (!t.isNode(current.object))
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
"Unsupported member base.",
|
||||
);
|
||||
const member = semanticStaticPropertyKey(
|
||||
current.property,
|
||||
current.computed,
|
||||
);
|
||||
const member =
|
||||
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
|
||||
if (typeof member !== "string")
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
"Dynamic provenance member.",
|
||||
);
|
||||
const base = provenanceForExpression(node.object, nestedContext(context));
|
||||
if (member === null)
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
"Dynamic provenance member.",
|
||||
);
|
||||
members.push(member);
|
||||
current = current.object;
|
||||
}
|
||||
const base = provenanceForExpression(current, nestedContext(context));
|
||||
const path = members.reverse();
|
||||
return base.status !== "module"
|
||||
? base
|
||||
: semanticOriginsProvenance(
|
||||
base.origins.map((origin) => ({
|
||||
...origin,
|
||||
importedPath: [...origin.importedPath, member],
|
||||
importedPath: [...origin.importedPath, ...path],
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -480,33 +503,6 @@ const provenanceForExpression = (
|
||||
return semanticLocalProvenance();
|
||||
};
|
||||
|
||||
const requireOrigin = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptModuleOrigin | undefined => {
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
if (!t.isNode(node.object)) return undefined;
|
||||
const nested = requireOrigin(node.object, state);
|
||||
const member =
|
||||
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
|
||||
return nested === undefined || typeof member !== "string"
|
||||
? undefined
|
||||
: { ...nested, importedPath: [...nested.importedPath, member] };
|
||||
}
|
||||
if (
|
||||
!t.isCallExpression(node) ||
|
||||
!t.isIdentifier(node.callee, { name: "require" })
|
||||
)
|
||||
return undefined;
|
||||
if (
|
||||
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
|
||||
semanticResolutionBlocked(state, node.callee, "require")
|
||||
)
|
||||
return undefined;
|
||||
const specifier = stringValue(node.arguments[0]);
|
||||
return specifier === undefined ? undefined : { specifier, importedPath: [] };
|
||||
};
|
||||
|
||||
const mergeValues = (
|
||||
values: readonly JavaScriptSemanticValue[],
|
||||
): JavaScriptSemanticValue => {
|
||||
|
||||
@@ -520,19 +520,23 @@ export const storageKind = (
|
||||
};
|
||||
|
||||
/** Produce a dotted callee name from member syntax. */
|
||||
export const calleeName = (node: t.Node): string => calleeNameAt(node);
|
||||
|
||||
const calleeNameAt = (node: t.Node): string => {
|
||||
if (t.isIdentifier(node)) return node.name;
|
||||
if (t.isImport(node)) return "import";
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const object = t.isNode(node.object) ? calleeNameAt(node.object) : "";
|
||||
// A dynamic key commits no name: `target[method]()` must not be reported as
|
||||
// `target.method`, which would fabricate a runtime call shape.
|
||||
const property = memberPropertyName(node);
|
||||
return object === "" ? property : `${object}.${property}`;
|
||||
export const calleeName = (node: t.Node): string => {
|
||||
const segments: string[] = [];
|
||||
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
// Dynamic keys retain their syntax identity instead of inventing a name.
|
||||
segments.push(memberPropertyName(node));
|
||||
if (!t.isNode(node.object)) break;
|
||||
node = node.object;
|
||||
}
|
||||
return "";
|
||||
const base = t.isIdentifier(node)
|
||||
? node.name
|
||||
: t.isImport(node)
|
||||
? "import"
|
||||
: "";
|
||||
segments.push(base);
|
||||
segments.reverse();
|
||||
const first = segments.findIndex((segment) => segment !== "");
|
||||
return first === -1 ? "" : segments.slice(first).join(".");
|
||||
};
|
||||
|
||||
/** Read a member property only when its syntax commits an exact name. */
|
||||
|
||||
Reference in new issue
Block a user