svelte-eslint-parser
Version:
Svelte parser for ESLint
274 lines (273 loc) • 9.65 kB
JavaScript
import { getFallbackKeys, traverseNodes } from "../traverse.js";
import { getEspree } from "../parser/espree.js";
import { analyze } from "eslint-scope";
import { findVariable } from "../scope/index.js";
export function parseConfig(code) {
const espree = getEspree();
const ast = espree.parse(code, {
range: true,
loc: true,
ecmaVersion: espree.latestEcmaVersion,
sourceType: "module",
});
// Set parent nodes.
traverseNodes(ast, {
enterNode(node, parent) {
node.parent = parent;
},
leaveNode() {
/* do nothing */
},
});
// Analyze scopes.
const scopeManager = analyze(ast, {
ignoreEval: true,
nodejsScope: false,
ecmaVersion: espree.latestEcmaVersion,
sourceType: "module",
fallback: getFallbackKeys,
});
return parseAst(ast, scopeManager);
}
function parseAst(ast, scopeManager) {
const edd = ast.body.find((node) => node.type === "ExportDefaultDeclaration");
if (!edd)
return {};
const decl = edd.declaration;
if (decl.type === "ClassDeclaration" || decl.type === "FunctionDeclaration")
return {};
return parseSvelteConfigExpression(decl, scopeManager);
}
function parseSvelteConfigExpression(node, scopeManager) {
const evaluated = evaluateExpression(node, scopeManager);
if (evaluated?.type !== 1 /* EvaluatedType.object */)
return {};
const result = {};
// Returns only known properties.
const compilerOptions = evaluated.getProperty("compilerOptions");
if (compilerOptions?.type === 1 /* EvaluatedType.object */) {
result.compilerOptions = {};
const runes = compilerOptions.getProperty("runes")?.getStatic();
if (runes?.value != null) {
result.compilerOptions.runes = Boolean(runes.value);
}
}
const kit = evaluated.getProperty("kit");
if (kit?.type === 1 /* EvaluatedType.object */) {
result.kit = {};
const files = kit.getProperty("files")?.getStatic();
if (files?.value != null)
result.kit.files = files.value;
}
return result;
}
class EvaluatedLiteral {
constructor(value) {
this.type = 0 /* EvaluatedType.literal */;
this.value = value;
}
getStatic() {
return this;
}
}
/** Evaluating an object expression. */
class EvaluatedObject {
constructor(node, parseExpression) {
this.type = 1 /* EvaluatedType.object */;
this.cached = new Map();
this.node = node;
this.parseExpression = parseExpression;
}
/** Gets the evaluated value of the property with the given name. */
getProperty(key) {
return this.withCache(key, () => {
let unknown = false;
for (const prop of [...this.node.properties].reverse()) {
if (prop.type === "Property") {
const name = this.getKey(prop);
if (name === key)
return this.parseExpression(prop.value);
if (name == null)
unknown = true;
}
else if (prop.type === "SpreadElement") {
const evaluated = this.parseExpression(prop.argument);
if (evaluated?.type === 1 /* EvaluatedType.object */) {
const value = evaluated.getProperty(key);
if (value)
return value;
}
unknown = true;
}
}
return unknown ? null : new EvaluatedLiteral(undefined);
});
}
getStatic() {
const object = {};
for (const prop of this.node.properties) {
if (prop.type === "Property") {
const name = this.getKey(prop);
if (name == null)
return null;
const evaluated = this.withCache(name, () => this.parseExpression(prop.value))?.getStatic();
if (!evaluated)
return null;
object[name] = evaluated.value;
}
else if (prop.type === "SpreadElement") {
const evaluated = this.parseExpression(prop.argument)?.getStatic();
if (!evaluated)
return null;
Object.assign(object, evaluated.value);
}
}
return { value: object };
}
withCache(key, parse) {
if (this.cached.has(key))
return this.cached.get(key) || null;
const evaluated = parse();
this.cached.set(key, evaluated);
return evaluated;
}
getKey(node) {
if (!node.computed && node.key.type === "Identifier")
return node.key.name;
const evaluatedKey = this.parseExpression(node.key)?.getStatic();
if (evaluatedKey)
return String(evaluatedKey.value);
return null;
}
}
function evaluateExpression(node, scopeManager) {
const tracked = new Map();
return parseExpression(node);
function parseExpression(node) {
if (node.type === "Literal") {
return new EvaluatedLiteral(node.value);
}
if (node.type === "Identifier") {
return parseIdentifier(node);
}
if (node.type === "ObjectExpression") {
return new EvaluatedObject(node, parseExpression);
}
return null;
}
function parseIdentifier(node) {
const defs = getIdentifierDefinitions(node);
if (defs.length !== 1) {
if (defs.length === 0 && node.name === "undefined")
return new EvaluatedLiteral(undefined);
return null;
}
const def = defs[0];
if (def.type !== "Variable")
return null;
if (def.parent.kind !== "const" || !def.node.init)
return null;
const evaluated = parseExpression(def.node.init);
if (!evaluated)
return null;
const assigns = parsePatternAssign(def.name, def.node.id);
let result = evaluated;
while (assigns.length) {
const assign = assigns.shift();
if (assign.type === "member") {
if (result.type !== 1 /* EvaluatedType.object */)
return null;
const next = result.getProperty(assign.name);
if (!next)
return null;
result = next;
}
else if (assign.type === "assignment") {
if (result.type === 0 /* EvaluatedType.literal */ &&
result.value === undefined) {
const next = parseExpression(assign.node.right);
if (!next)
return null;
result = next;
}
}
}
return result;
}
function getIdentifierDefinitions(node) {
if (tracked.has(node))
return tracked.get(node);
tracked.set(node, []);
const defs = findVariable(scopeManager, node)?.defs;
if (!defs)
return [];
tracked.set(node, defs);
if (defs.length !== 1) {
const def = defs[0];
if (def.type === "Variable" &&
def.parent.kind === "const" &&
def.node.id.type === "Identifier" &&
def.node.init?.type === "Identifier") {
const newDef = getIdentifierDefinitions(def.node.init);
tracked.set(node, newDef);
return newDef;
}
}
return defs;
}
}
/**
* Returns the assignment path.
* For example,
* `let {a: {target}} = {}`
* -> `[{type: "member", name: 'a'}, {type: "member", name: 'target'}]`.
* `let {a: {target} = foo} = {}`
* -> `[{type: "member", name: 'a'}, {type: "assignment"}, {type: "member", name: 'target'}]`.
*/
function parsePatternAssign(node, root) {
return parse(root) || [];
function parse(target) {
if (node === target) {
return [];
}
if (target.type === "Identifier") {
return null;
}
if (target.type === "AssignmentPattern") {
const left = parse(target.left);
if (!left)
return null;
return [{ type: "assignment", node: target }, ...left];
}
if (target.type === "ObjectPattern") {
for (const prop of target.properties) {
if (prop.type === "Property") {
const name = !prop.computed && prop.key.type === "Identifier"
? prop.key.name
: prop.key.type === "Literal"
? String(prop.key.value)
: null;
if (!name)
continue;
const value = parse(prop.value);
if (!value)
return null;
return [{ type: "member", name }, ...value];
}
}
return null;
}
if (target.type === "ArrayPattern") {
for (const [index, element] of target.elements.entries()) {
if (!element)
continue;
const value = parse(element);
if (!value)
return null;
return [{ type: "member", name: String(index) }, ...value];
}
return null;
}
return null;
}
}