/* 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 . */
// @flow
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";
import type { SimplePath, Node, TraversalAncestors } from "./utils/simple-path";
import type { SourceId } from "../../types";
import type { AstPosition, AstLocation } from "./types";
export type SymbolDeclaration = {
name: string,
location: AstLocation,
generatedLocation?: AstPosition,
};
export type ClassDeclaration = SymbolDeclaration & {
parent: ?{|
name: string,
location: AstLocation,
|},
};
export type FunctionDeclaration = SymbolDeclaration & {
parameterNames: string[],
klass: string | null,
identifier: Object,
index: number,
};
export type CallDeclaration = SymbolDeclaration & {
values: string[],
};
export type MemberDeclaration = SymbolDeclaration & {
computed: Boolean,
expression: string,
};
export type IdentifierDeclaration = {
name: string,
location: AstLocation,
expression: string,
};
export type ImportDeclaration = {
source: string,
location: AstLocation,
specifiers: string[],
};
export type SymbolDeclarations = {|
classes: Array,
functions: Array,
memberExpressions: Array,
callExpressions: Array,
objectProperties: Array,
identifiers: Array,
imports: Array,
comments: Array,
literals: Array,
hasJsx: boolean,
hasTypes: boolean,
framework: ?string,
loading: false,
|};
let symbolDeclarations: Map = new Map();
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;
}
// eslint-disable-next-line complexity
function extractSymbol(path: SimplePath, symbols, state) {
if (isFunction(path)) {
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,
});
}
if (t.isJSXElement(path)) {
symbols.hasJsx = true;
}
if (t.isGenericTypeAnnotation(path)) {
symbols.hasTypes = true;
}
if (t.isClassDeclaration(path)) {
const { loc, superClass } = path.node;
symbols.classes.push({
name: path.node.id.name,
parent: superClass
? {
name: t.isMemberExpression(superClass)
? getCode(superClass)
: superClass.name,
location: superClass.loc,
}
: null,
location: loc,
});
}
if (t.isImportDeclaration(path)) {
symbols.imports.push({
source: path.node.source.value,
location: path.node.loc,
specifiers: getSpecifiers(path.node.specifiers),
});
}
if (t.isObjectProperty(path)) {
const { start, end, identifierName } = path.node.key.loc;
symbols.objectProperties.push({
name: identifierName,
location: { start, end },
expression: getSnippet(path),
});
}
if (t.isMemberExpression(path) || t.isOptionalMemberExpression(path)) {
const { start, end } = path.node.property.loc;
symbols.memberExpressions.push({
name: path.node.property.name,
location: { start, end },
expression: getSnippet(path),
computed: path.node.computed,
});
}
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)) {
const { callee } = path.node;
const args = path.node.arguments;
if (t.isMemberExpression(callee)) {
const {
property: { name, loc },
} = callee;
symbols.callExpressions.push({
name,
values: args.filter(arg => arg.value).map(arg => arg.value),
location: loc,
});
} else {
const { start, end, identifierName } = callee.loc;
symbols.callExpressions.push({
name: identifierName,
values: args.filter(arg => arg.value).map(arg => arg.value),
location: { start, end },
});
}
}
if (t.isStringLiteral(path) && t.isProperty(path.parentPath)) {
const { start, end } = path.node.loc;
return symbols.identifiers.push({
name: path.node.value,
expression: getObjectExpressionValue(path.parent),
location: { start, end },
});
}
if (t.isIdentifier(path) && !t.isGenericTypeAnnotation(path.parent)) {
let { start, end } = path.node.loc;
// We want to include function params, but exclude the function name
if (t.isClassMethod(path.parent) && !path.inList) {
return;
}
if (t.isProperty(path.parentPath) && !isObjectShorthand(path.parent)) {
return symbols.identifiers.push({
name: path.node.name,
expression: getObjectExpressionValue(path.parent),
location: { start, end },
});
}
if (path.node.typeAnnotation) {
const { column } = path.node.typeAnnotation.loc.start;
end = { ...end, column };
}
symbols.identifiers.push({
name: path.node.name,
expression: path.node.name,
location: { start, end },
});
}
if (t.isThisExpression(path.node)) {
const { start, end } = path.node.loc;
symbols.identifiers.push({
name: "this",
location: { start, end },
expression: "this",
});
}
if (t.isVariableDeclarator(path)) {
const nodeId = path.node.id;
symbols.identifiers.push(...getPatternIdentifiers(nodeId));
}
}
function extractSymbols(sourceId): SymbolDeclarations {
const symbols = {
functions: [],
callExpressions: [],
memberExpressions: [],
objectProperties: [],
comments: [],
identifiers: [],
classes: [],
imports: [],
literals: [],
hasJsx: false,
hasTypes: false,
loading: false,
framework: undefined,
};
const state = {
fnCounts: Object.create(null),
};
const ast = traverseAst(sourceId, {
enter(node: Node, ancestors: TraversalAncestors) {
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: string,
expression: string,
path?: { node: Node },
prevPath?: SimplePath
) {
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: Node,
expression: string = "",
optional = false
) {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const 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: ?SimplePath,
prevPath?: SimplePath,
expression?: string = ""
) {
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: SimplePath,
prevPath: SimplePath,
expression: string
) {
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: SimplePath | null,
prevPath?: SimplePath,
expression?: string = ""
): string {
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: SourceId): SymbolDeclarations {
if (symbolDeclarations.has(sourceId)) {
const symbols = symbolDeclarations.get(sourceId);
if (symbols) {
return symbols;
}
}
const symbols = extractSymbols(sourceId);
symbolDeclarations.set(sourceId, symbols);
return symbols;
}