es-check
Version:
Checks the ECMAScript version of .js glob against a specified version of ECMAScript with a shell command
169 lines (144 loc) • 3.93 kB
JavaScript
const { ES_FEATURES } = require("../constants");
const { checkMap } = require("./ast");
const FUNCTION_TYPES = new Set([
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
"MethodDefinition",
]);
const CHILD_KEYS = [
"body",
"declarations",
"expression",
"left",
"right",
"argument",
"arguments",
"callee",
"object",
"property",
"properties",
"elements",
"params",
"id",
"init",
"test",
"consequent",
"alternate",
"cases",
"discriminant",
"block",
"handler",
"finalizer",
"source",
"specifiers",
"declaration",
"exported",
"imported",
"local",
"key",
"value",
"superClass",
];
function normalizeNodeType(nodeType) {
if (nodeType === "ExportDeclaration") {
return [
"ExportNamedDeclaration",
"ExportDefaultDeclaration",
"ExportAllDeclaration",
];
}
if (nodeType === "BigIntLiteral") {
return ["Literal"];
}
return [nodeType];
}
function buildFeatureIndex(features) {
const index = {};
for (const [name, { astInfo }] of Object.entries(features)) {
if (!astInfo?.nodeType) continue;
const types = normalizeNodeType(astInfo.nodeType);
for (const t of types) {
(index[t] ||= []).push({ name, astInfo });
}
}
return index;
}
const featuresByNodeType = buildFeatureIndex(ES_FEATURES);
function matchesFeature(node, astInfo, context = {}) {
if (astInfo.childType) {
return node.elements?.some((el) => el?.type === astInfo.childType) || false;
}
if (astInfo.name && node.type === "Identifier") {
return node.name === astInfo.name;
}
if (astInfo.nodeType === "BigIntLiteral") {
return node.bigint !== undefined;
}
if (astInfo.kind && node.kind !== astInfo.kind) {
return false;
}
const hasOperatorMismatch =
(astInfo.operator && node.operator !== astInfo.operator) ||
(astInfo.operators && !astInfo.operators.includes(node.operator));
if (hasOperatorMismatch) {
return false;
}
if (astInfo.property === "superClass") {
return node.superClass !== null;
}
if (astInfo.topLevel && node.type === "AwaitExpression") {
return context.isTopLevel === true;
}
if (astInfo.leftIsPrivate && node.type === "BinaryExpression") {
return node.left?.type === "PrivateIdentifier";
}
if (node.type === "CallExpression" || node.type === "NewExpression") {
return checkMap(node, astInfo);
}
return true;
}
function detectFeaturesFromAST(ast) {
const foundFeatures = Object.create(null);
for (const key of Object.keys(ES_FEATURES)) {
foundFeatures[key] = false;
}
const remaining = new Set(Object.keys(ES_FEATURES));
const stack = [{ node: ast, functionDepth: 0 }];
while (stack.length > 0 && remaining.size > 0) {
const { node, functionDepth } = stack.pop();
if (!node || typeof node !== "object") continue;
const isFunction = FUNCTION_TYPES.has(node.type);
const newFunctionDepth = isFunction ? functionDepth + 1 : functionDepth;
const candidates = featuresByNodeType[node.type];
if (candidates) {
const context = { isTopLevel: functionDepth === 0 };
for (const { name, astInfo } of candidates) {
if (!remaining.has(name)) continue;
if (matchesFeature(node, astInfo, context)) {
foundFeatures[name] = true;
remaining.delete(name);
}
}
}
for (const key of CHILD_KEYS) {
const child = node[key];
if (!child) continue;
if (Array.isArray(child)) {
for (let i = child.length - 1; i >= 0; i--) {
if (child[i])
stack.push({ node: child[i], functionDepth: newFunctionDepth });
}
} else if (child.type) {
stack.push({ node: child, functionDepth: newFunctionDepth });
}
}
}
return foundFeatures;
}
module.exports = {
normalizeNodeType,
buildFeatureIndex,
matchesFeature,
detectFeaturesFromAST,
};