lion/packages-node/providence-analytics/src/program/analyzers/find-exports.js
2023-11-09 11:38:39 +01:00

258 lines
8.9 KiB
JavaScript

/* eslint-disable no-shadow, no-param-reassign */
import path from 'path';
import { swcTraverse } from '../utils/swc-traverse.js';
import { getAssertionType } from '../utils/get-assertion-type.js';
import { Analyzer } from '../core/Analyzer.js';
import { trackDownIdentifier } from './helpers/track-down-identifier.js';
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-declaration.js';
import { LogService } from '../core/LogService.js';
/**
* @typedef {import("@swc/core").Module} SwcAstModule
* @typedef {import("@swc/core").Node} SwcNode
* @typedef {import("@swc/core").VariableDeclaration} SwcVariableDeclaration
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').SwcScope} SwcScope
* @typedef {import('../../../types/index.js').SwcBinding} SwcBinding
* @typedef {import('../../../types/index.js').SwcPath} SwcPath
* @typedef {import('../../../types/index.js').SwcVisitor} SwcVisitor
* @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile
* @typedef {object} RootFileMapEntry
* @typedef {string} currentFileSpecifier this is the local name in the file we track from
* @typedef {RootFile} rootFile contains file(filePath) and specifier
* @typedef {RootFileMapEntry[]} RootFileMap
* @typedef {{ exportSpecifiers:string[]; localMap: object; source:string, __tmp: { path:string } }} FindExportsSpecifierObj
*/
/**
* @param {FindExportsSpecifierObj[]} transformedFile
*/
async function trackdownRoot(transformedFile, relativePath, projectPath) {
const fullCurrentFilePath = path.resolve(projectPath, relativePath);
for (const specObj of transformedFile) {
/** @type {RootFileMap} */
const rootFileMap = [];
if (specObj.exportSpecifiers[0] === '[file]') {
rootFileMap.push(undefined);
} else {
/**
* './src/origin.js': `export class MyComp {}`
* './index.js:' `export { MyComp as RenamedMyComp } from './src/origin'`
*
* Goes from specifier like 'RenamedMyComp' to object for rootFileMap like:
* {
* currentFileSpecifier: 'RenamedMyComp',
* rootFile: {
* file: './src/origin.js',
* specifier: 'MyCompDefinition',
* }
* }
*/
for (const specifier of specObj.exportSpecifiers) {
let rootFile;
let localMapMatch;
if (specObj.localMap) {
localMapMatch = specObj.localMap.find(m => m.exported === specifier);
}
// TODO: find out if possible to use trackDownIdentifierFromScope
if (specObj.source) {
// TODO: see if still needed: && (localMapMatch || specifier === '[default]')
const importedIdentifier = localMapMatch?.local || specifier;
rootFile = await trackDownIdentifier(
specObj.source,
importedIdentifier,
fullCurrentFilePath,
projectPath,
);
/** @type {RootFileMapEntry} */
const entry = {
currentFileSpecifier: specifier,
rootFile,
};
rootFileMap.push(entry);
} else {
/** @type {RootFileMapEntry} */
const entry = {
currentFileSpecifier: specifier,
rootFile: { file: '[current]', specifier },
};
rootFileMap.push(entry);
}
}
}
specObj.rootFileMap = rootFileMap;
}
return transformedFile;
}
function cleanup(transformedFile) {
transformedFile.forEach(specObj => {
if (specObj.__tmp) {
delete specObj.__tmp;
}
});
return transformedFile;
}
/**
* @param {*} node
* @returns {string[]}
*/
function getExportSpecifiers(node) {
// handles default [export const g = 4];
if (node.declaration) {
if (node.declaration.declarations) {
return [node.declaration.declarations[0].id.value];
}
if (node.declaration.identifier) {
return [node.declaration.identifier.value];
}
}
// handles (re)named specifiers [export { x (as y)} from 'y'];
return (node.specifiers || []).map(s => {
if (s.exported) {
// { x as y }
return s.exported.value === 'default' ? '[default]' : s.exported.value;
}
// { x }
return s.orig.value;
});
}
/**
* @returns {{local:string;exported:string;}|undefined[]}
*/
function getLocalNameSpecifiers(node) {
return (node.declaration?.declarations || node.specifiers || [])
.map(s => {
if (s.exported && s.orig && s.exported.value !== s.orig.value) {
return {
// if reserved keyword 'default' is used, translate it into 'providence keyword'
local: s.orig.value === 'default' ? '[default]' : s.orig.value,
exported: s.exported.value,
};
}
return undefined;
})
.filter(Boolean);
}
const isImportingSpecifier = pathOrNode =>
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
/**
* Finds import specifiers and sources for a given ast result
* @param {SwcAstModule} swcAst
* @param {FindExportsConfig} config
*/
function findExportsPerAstFile(swcAst, { skipFileImports }) {
LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`);
// Visit AST...
/** @type {FindExportsSpecifierObj[]} */
const transformedFile = [];
// Unfortunately, we cannot have async functions in babel traverse.
// Therefore, we store a temp reference to path that we use later for
// async post processing (tracking down original export Identifier)
/** @type {{[key:string]:SwcBinding}} */
let globalScopeBindings;
const exportHandler = (/** @type {SwcPath} */ astPath) => {
const exportSpecifiers = getExportSpecifiers(astPath.node);
const localMap = getLocalNameSpecifiers(astPath.node);
const source = astPath.node.source?.value;
const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } };
const assertionType = getAssertionType(astPath.node);
if (assertionType) {
entry.assertionType = assertionType;
}
transformedFile.push(entry);
};
const exportDefaultHandler = (/** @type {SwcPath} */ astPath) => {
const exportSpecifiers = ['[default]'];
let source;
// Is it an inline declaration like "export default class X {};" ?
if (
astPath.node.decl?.type === 'Identifier' ||
astPath.node.expression?.type === 'Identifier'
) {
// It is a reference to an identifier like "export { x } from 'y';"
const importOrDeclPath = getReferencedDeclaration({
referencedIdentifierName: astPath.node.decl?.value || astPath.node.expression.value,
globalScopeBindings,
});
if (isImportingSpecifier(importOrDeclPath)) {
source = importOrDeclPath.parentPath.node.source.value;
}
}
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath } });
};
/** @type {SwcVisitor} */
const visitor = {
Module({ scope }) {
globalScopeBindings = scope.bindings;
},
ExportDeclaration: exportHandler,
ExportNamedDeclaration: exportHandler,
ExportDefaultDeclaration: exportDefaultHandler,
ExportDefaultExpression: exportDefaultHandler,
};
swcTraverse(swcAst, visitor, { needsAdvancedPaths: true });
if (!skipFileImports) {
// Always add an entry for just the file 'relativePath'
// (since this also can be imported directly from a search target project)
transformedFile.push({
exportSpecifiers: ['[file]'],
// source: relativePath,
});
}
return transformedFile;
}
export default class FindExportsAnalyzer extends Analyzer {
static analyzerName = /** @type {AnalyzerName} */ ('find-exports');
static requiredAst = /** @type {AnalyzerAst} */ ('swc');
/**
* @typedef FindExportsConfig
* @property {boolean} [onlyInternalSources=false]
* @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like
* [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result,
* not list file exports
*/
get config() {
return {
targetProjectPath: null,
skipFileImports: false,
...this._customConfig,
};
}
static async analyzeFile(ast, { relativePath, analyzerCfg }) {
const projectPath = analyzerCfg.targetProjectPath;
let transformedFile = findExportsPerAstFile(ast, analyzerCfg);
transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath);
transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath);
transformedFile = cleanup(transformedFile);
return { result: transformedFile };
}
}