/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at . */
import * as t from "@babel/types";
import createSimplePath from "./utils/simple-path";
import { traverseAst } from "./utils/ast";
import {
isFunction,
isObjectShorthand,
isComputedExpression,
getObjectExpressionValue,
getPatternIdentifiers,
getComments,
getSpecifiers,
getCode,
nodeLocationKey,
getFunctionParameterNames,
} from "./utils/helpers";
import { inferClassName } from "./utils/inferClassName";
import getFunctionName from "./utils/getFunctionName";
import { getFramework } from "./frameworks";
let symbolDeclarations = new Map();
function extractFunctionSymbol(path, state, symbols) {
const name = getFunctionName(path.node, path.parent);
if (!state.fnCounts[name]) {
state.fnCounts[name] = 0;
}
const index = state.fnCounts[name]++;
symbols.functions.push({
name,
klass: inferClassName(path),
location: path.node.loc,
parameterNames: getFunctionParameterNames(path),
identifier: path.node.id,
// indicates the occurence of the function in a file
// e.g { name: foo, ... index: 4 } is the 4th foo function
// in the file
index,
});
}
function extractSymbol(path, symbols, state) {
if (isFunction(path)) {
extractFunctionSymbol(path, state, symbols);
}
if (t.isJSXElement(path)) {
symbols.hasJsx = true;
}
if (t.isGenericTypeAnnotation(path)) {
symbols.hasTypes = true;
}
if (t.isClassDeclaration(path)) {
symbols.classes.push(getClassDeclarationSymbol(path.node));
}
if (t.isImportDeclaration(path)) {
symbols.imports.push(getImportDeclarationSymbol(path.node));
}
if (t.isObjectProperty(path)) {
symbols.objectProperties.push(getObjectPropertySymbol(path));
}
if (t.isMemberExpression(path) || t.isOptionalMemberExpression(path)) {
symbols.memberExpressions.push(getMemberExpressionSymbol(path));
}
if (
(t.isStringLiteral(path) || t.isNumericLiteral(path)) &&
t.isMemberExpression(path.parentPath)
) {
// We only need literals that are part of computed memeber expressions
const { start, end } = path.node.loc;
symbols.literals.push({
name: path.node.value,
location: { start, end },
expression: getSnippet(path.parentPath),
});
}
if (t.isCallExpression(path)) {
symbols.callExpressions.push(getCallExpressionSymbol(path.node));
}
symbols.identifiers.push(...getIdentifierSymbols(path));
}
function extractSymbols(sourceId) {
const symbols = {
functions: [],
callExpressions: [],
memberExpressions: [],
objectProperties: [],
comments: [],
identifiers: [],
classes: [],
imports: [],
literals: [],
hasJsx: false,
hasTypes: false,
framework: undefined,
};
const state = {
fnCounts: Object.create(null),
};
const ast = traverseAst(sourceId, {
enter(node, ancestors) {
try {
const path = createSimplePath(ancestors);
if (path) {
extractSymbol(path, symbols, state);
}
} catch (e) {
console.error(e);
}
},
});
// comments are extracted separately from the AST
symbols.comments = getComments(ast);
symbols.identifiers = getUniqueIdentifiers(symbols.identifiers);
symbols.framework = getFramework(symbols);
return symbols;
}
function extendSnippet(name, expression, path, prevPath) {
const computed = path?.node.computed;
const optional = path?.node.optional;
const prevComputed = prevPath?.node.computed;
const prevArray = t.isArrayExpression(prevPath);
const array = t.isArrayExpression(path);
const value = path?.node.property?.extra?.raw || "";
if (expression === "") {
if (computed) {
return name === undefined ? `[${value}]` : `[${name}]`;
}
return name;
}
if (computed || array) {
if (prevComputed || prevArray) {
return `[${name}]${expression}`;
}
return `[${name === undefined ? value : name}].${expression}`;
}
if (prevComputed || prevArray) {
return `${name}${expression}`;
}
if (isComputedExpression(expression) && name !== undefined) {
return `${name}${expression}`;
}
if (optional) {
return `${name}?.${expression}`;
}
return `${name}.${expression}`;
}
function getMemberSnippet(node, expression = "", optional = false) {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const name = t.isPrivateName(node.property)
? `#${node.property.id.name}`
: node.property.name;
const snippet = getMemberSnippet(
node.object,
extendSnippet(name, expression, { node }),
node.optional
);
return snippet;
}
if (t.isCallExpression(node)) {
return "";
}
if (t.isThisExpression(node)) {
return `this.${expression}`;
}
if (t.isIdentifier(node)) {
if (isComputedExpression(expression)) {
return `${node.name}${expression}`;
}
if (optional) {
return `${node.name}?.${expression}`;
}
return `${node.name}.${expression}`;
}
return expression;
}
function getObjectSnippet(path, prevPath, expression = "") {
if (!path) {
return expression;
}
const { name } = path.node.key;
const extendedExpression = extendSnippet(name, expression, path, prevPath);
const nextPrevPath = path;
const nextPath = path.parentPath && path.parentPath.parentPath;
return getSnippet(nextPath, nextPrevPath, extendedExpression);
}
function getArraySnippet(path, prevPath, expression) {
if (!prevPath.parentPath) {
throw new Error("Assertion failure - path should exist");
}
const index = `${prevPath.parentPath.containerIndex}`;
const extendedExpression = extendSnippet(index, expression, path, prevPath);
const nextPrevPath = path;
const nextPath = path.parentPath && path.parentPath.parentPath;
return getSnippet(nextPath, nextPrevPath, extendedExpression);
}
function getSnippet(path, prevPath, expression = "") {
if (!path) {
return expression;
}
if (t.isVariableDeclaration(path)) {
const node = path.node.declarations[0];
const { name } = node.id;
return extendSnippet(name, expression, path, prevPath);
}
if (t.isVariableDeclarator(path)) {
const node = path.node.id;
if (t.isObjectPattern(node)) {
return expression;
}
const prop = extendSnippet(node.name, expression, path, prevPath);
return prop;
}
if (t.isAssignmentExpression(path)) {
const node = path.node.left;
const name = t.isMemberExpression(node)
? getMemberSnippet(node)
: node.name;
const prop = extendSnippet(name, expression, path, prevPath);
return prop;
}
if (isFunction(path)) {
return expression;
}
if (t.isIdentifier(path)) {
return `${path.node.name}.${expression}`;
}
if (t.isObjectProperty(path)) {
return getObjectSnippet(path, prevPath, expression);
}
if (t.isObjectExpression(path)) {
const parentPath = prevPath?.parentPath;
return getObjectSnippet(parentPath, prevPath, expression);
}
if (t.isMemberExpression(path) || t.isOptionalMemberExpression(path)) {
return getMemberSnippet(path.node, expression);
}
if (t.isArrayExpression(path)) {
if (!prevPath) {
throw new Error("Assertion failure - path should exist");
}
return getArraySnippet(path, prevPath, expression);
}
return "";
}
export function clearSymbols() {
symbolDeclarations = new Map();
}
export function getSymbols(sourceId) {
if (symbolDeclarations.has(sourceId)) {
const symbols = symbolDeclarations.get(sourceId);
if (symbols) {
return symbols;
}
}
const symbols = extractSymbols(sourceId);
symbolDeclarations.set(sourceId, symbols);
return symbols;
}
function getUniqueIdentifiers(identifiers) {
const newIdentifiers = [];
const locationKeys = new Set();
for (const newId of identifiers) {
const key = nodeLocationKey(newId);
if (!locationKeys.has(key)) {
locationKeys.add(key);
newIdentifiers.push(newId);
}
}
return newIdentifiers;
}
function getMemberExpressionSymbol(path) {
const { start, end } = path.node.property.loc;
return {
name: t.isPrivateName(path.node.property)
? `#${path.node.property.id.name}`
: path.node.property.name,
location: { start, end },
expression: getSnippet(path),
computed: path.node.computed,
};
}
function getImportDeclarationSymbol(node) {
return {
source: node.source.value,
location: node.loc,
specifiers: getSpecifiers(node.specifiers),
};
}
function getObjectPropertySymbol(path) {
const { start, end, identifierName } = path.node.key.loc;
return {
name: identifierName,
location: { start, end },
expression: getSnippet(path),
};
}
function getCallExpressionSymbol(node) {
const { callee, arguments: args } = node;
const values = args.filter(arg => arg.value).map(arg => arg.value);
if (t.isMemberExpression(callee)) {
const {
property: { name, loc },
} = callee;
return {
name,
values,
location: loc,
};
}
const { start, end, identifierName } = callee.loc;
return {
name: identifierName,
values,
location: { start, end },
};
}
function getClassParentName(superClass) {
return t.isMemberExpression(superClass)
? getCode(superClass)
: superClass.name;
}
function getClassParentSymbol(superClass) {
if (!superClass) {
return null;
}
return {
name: getClassParentName(superClass),
location: superClass.loc,
};
}
function getClassDeclarationSymbol(node) {
const { loc, superClass } = node;
return {
name: node.id.name,
parent: getClassParentSymbol(superClass),
location: loc,
};
}
/**
* Get a list of identifiers that are part of the given path.
*
* @param {Object} path
* @returns {Array.