lion/packages-node/providence-analytics/src/program/analyzers/match-imports.js

236 lines
9.1 KiB
JavaScript

/* eslint-disable no-continue */
import pathLib from 'path';
/* eslint-disable no-shadow, no-param-reassign */
import FindImportsAnalyzer from './find-imports.js';
import FindExportsAnalyzer from './find-exports.js';
import { Analyzer } from '../core/Analyzer.js';
import { fromImportToExportPerspective } from './helpers/from-import-to-export-perspective.js';
import { transformIntoIterableFindExportsOutput } from './helpers/transform-into-iterable-find-exports-output.js';
import { transformIntoIterableFindImportsOutput } from './helpers/transform-into-iterable-find-imports-output.js';
/**
* @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../types/index.js').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @typedef {import('../../../types/index.js').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
* @typedef {import('../../../types/index.js').ConciseMatchImportsAnalyzerResult} ConciseMatchImportsAnalyzerResult
* @typedef {import('../../../types/index.js').MatchImportsConfig} MatchImportsConfig
* @typedef {import('../../../types/index.js').MatchImportsAnalyzerResult} MatchImportsAnalyzerResult
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../../../types/index.js').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').AnalyzerAst} AnalyzerAst
*/
/**
* Needed in case fromImportToExportPerspective does not have a
* externalRootPath supplied.
* @param {string} exportPath exportEntry.file
* @param {PathRelativeFromProjectRoot} translatedImportPath result of fromImportToExportPerspective
*/
function compareImportAndExportPaths(exportPath, translatedImportPath) {
return (
exportPath === translatedImportPath ||
exportPath === `${translatedImportPath}.js` ||
exportPath === `${translatedImportPath}/index.js`
);
}
/**
* Makes a 'compatible resultsArray' (compatible with dashboard + tests + ...?) from
* a conciseResultsArray.
* @param {ConciseMatchImportsAnalyzerResult} conciseResultsArray
* @param {string} importProject
*/
function createCompatibleMatchImportsResult(conciseResultsArray, importProject) {
const compatibleResult = [];
for (const matchedExportEntry of conciseResultsArray) {
const [name, filePath, project] = matchedExportEntry.exportSpecifier.id.split('::');
const exportSpecifier = {
...matchedExportEntry.exportSpecifier,
name,
filePath,
project,
};
compatibleResult.push({
exportSpecifier,
matchesPerProject: [{ project: importProject, files: matchedExportEntry.importProjectFiles }],
});
}
return compatibleResult;
}
/**
* @param {FindExportsAnalyzerResult} exportsAnalyzerResult
* @param {FindImportsAnalyzerResult} importsAnalyzerResult
* @param {MatchImportsConfig} customConfig
* @returns {Promise<MatchImportsAnalyzerResult>}
*/
async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) {
const cfg = {
...customConfig,
};
// TODO: What if this info is retrieved from cached importProject/target project?
const importProjectPath = cfg.targetProjectPath;
// TODO: make find-import / export automatically output these, to improve perf...
const iterableFindExportsOutput = transformIntoIterableFindExportsOutput(exportsAnalyzerResult);
const iterableFindImportsOutput = transformIntoIterableFindImportsOutput(importsAnalyzerResult);
/** @type {ConciseMatchImportsAnalyzerResult} */
const conciseResultsArray = [];
for (const exportEntry of iterableFindExportsOutput) {
for (const importEntry of iterableFindImportsOutput) {
/**
* 1. Does target import ref specifier?
*
* Example context (read by 'find-imports'/'find-exports' analyzers)
* - export (/folder/exporting-file.js):
* `export const x = 'foo'`
* - import (target-project-a/importing-file.js):
* `import { x, y } from '@reference-repo/folder/exporting-file.js'`
* Example variables (extracted by 'find-imports'/'find-exports' analyzers)
* - exportSpecifier: 'x'
* - importSpecifiers: ['x', 'y']
* @type {boolean}
*/
const hasExportSpecifierImported =
exportEntry.specifier === importEntry.specifier || importEntry.specifier === '[*]';
if (!hasExportSpecifierImported) {
continue;
}
/**
* 2. Are we from the same source?
* A.k.a. is source required by target the same as the one found in target.
* (we know the specifier name is the same, now we need to check the file as well.)
*
* Example:
* exportFile './foo.js'
* => export const z = 'bar'
* importFile 'importing-target-project/file.js'
* => import { z } from '@reference/foo.js'
* @type {PathRelativeFromProjectRoot|null}
*/
const fromImportToExport = await fromImportToExportPerspective({
importee: importEntry.normalizedSource,
importer: /** @type {PathFromSystemRoot} */ (
pathLib.resolve(importProjectPath, importEntry.file)
),
importeeProjectPath: cfg.referenceProjectPath,
});
const isFromSameSource = compareImportAndExportPaths(exportEntry.file, fromImportToExport);
if (!isFromSameSource) {
continue;
}
/**
* 3. When above checks pass, we have a match.
* Add it to the results array
*/
const id = `${exportEntry.specifier}::${exportEntry.file}::${exportsAnalyzerResult.analyzerMeta.targetProject.name}`;
const resultForCurrentExport = conciseResultsArray.find(
entry => entry.exportSpecifier && entry.exportSpecifier.id === id,
);
if (resultForCurrentExport) {
// Prevent that we count double import like "import * as all from 'x'" and "import {smth} from 'x'"
if (!resultForCurrentExport.importProjectFiles.includes(importEntry.file)) {
resultForCurrentExport.importProjectFiles.push(importEntry.file);
}
} else {
conciseResultsArray.push({
exportSpecifier: { id, ...(exportEntry.meta ? { meta: exportEntry.meta } : {}) },
importProjectFiles: [importEntry.file],
});
}
}
}
const importProject = importsAnalyzerResult.analyzerMeta.targetProject.name;
return /** @type {AnalyzerQueryResult} */ (
createCompatibleMatchImportsResult(conciseResultsArray, importProject)
);
}
export default class MatchImportsAnalyzer extends Analyzer {
static analyzerName = /** @type {AnalyzerName} */ ('match-imports');
static requiredAst = /** @type {AnalyzerAst} */ ('swc');
static requiresReference = true;
/**
* Based on ExportsAnalyzerResult of reference project(s) (for instance lion-based-ui)
* and ImportsAnalyzerResult of search-targets (for instance my-app-using-lion-based-ui),
* an overview is returned of all matching imports and exports.
* @param {MatchImportsConfig} customConfig
*/
async execute(customConfig = {}) {
/**
* @typedef MatchImportsConfig
* @property {FindExportsConfig} [exportsConfig] These will be used when no exportsAnalyzerResult
* is provided (recommended way)
* @property {FindImportsConfig} [importsConfig]
* @property {GatherFilesConfig} [gatherFilesConfig]
* @property {array} [referenceProjectPath] reference paths
* @property {array} [targetProjectPath] search target paths
* @property {FindImportsAnalyzerResult} [targetProjectResult]
* @property {FindExportsAnalyzerResult} [referenceProjectResult]
*/
const cfg = {
gatherFilesConfig: {},
referenceProjectPath: null,
targetProjectPath: null,
targetProjectResult: null,
referenceProjectResult: null,
...customConfig,
};
/**
* Prepare
*/
const cachedAnalyzerResult = this._prepare(cfg);
if (cachedAnalyzerResult) {
return cachedAnalyzerResult;
}
/**
* Traverse
*/
let { referenceProjectResult } = cfg;
if (!referenceProjectResult) {
const findExportsAnalyzer = new FindExportsAnalyzer();
referenceProjectResult = await findExportsAnalyzer.execute({
metaConfig: cfg.metaConfig,
targetProjectPath: cfg.referenceProjectPath,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
}
let { targetProjectResult } = cfg;
if (!targetProjectResult) {
const findImportsAnalyzer = new FindImportsAnalyzer();
targetProjectResult = await findImportsAnalyzer.execute({
metaConfig: cfg.metaConfig,
targetProjectPath: cfg.targetProjectPath,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
}
const queryOutput = await matchImportsPostprocess(
referenceProjectResult,
targetProjectResult,
cfg,
);
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}