Merge pull request #1545 from ing-bank/feat/providenceTypes

chore: types for providence
This commit is contained in:
Thijs Louisse 2021-11-16 15:55:40 +01:00 committed by GitHub
commit 30e0829a61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 5172 additions and 612 deletions

View file

@ -0,0 +1,5 @@
---
'providence-analytics': patch
---
add type support for (the majority of) providence-analytics

File diff suppressed because it is too large Load diff

View file

@ -100,7 +100,7 @@ function targetDefault() {
// eslint-disable-next-line import/no-dynamic-require, global-require
const { name } = require(`${process.cwd()}/package.json`);
if (name === 'providence') {
return InputDataService.getTargetProjectPaths();
return InputDataService.targetProjectPaths;
}
return [toPosixPath(process.cwd())];
}

View file

@ -1,6 +1,3 @@
// @ts-ignore-next-line
require('../program/types/index.js');
const child_process = require('child_process'); // eslint-disable-line camelcase
const pathLib = require('path');
const commander = require('commander');

View file

@ -6,12 +6,12 @@ const { Analyzer } = require('./helpers/Analyzer.js');
const { trackDownIdentifierFromScope } = require('./helpers/track-down-identifier.js');
const { aForEach } = require('../utils/async-array-utils.js');
/** @typedef {import('./types').FindClassesAnalyzerOutput} FindClassesAnalyzerOutput */
/** @typedef {import('./types').FindClassesAnalyzerOutputEntry} FindClassesAnalyzerOutputEntry */
/** @typedef {import('./types').FindClassesConfig} FindClassesConfig */
/** @typedef {import('../types/analyzers').FindClassesAnalyzerOutput} FindClassesAnalyzerOutput */
/** @typedef {import('../types/analyzers').FindClassesAnalyzerOutputEntry} FindClassesAnalyzerOutputEntry */
/** @typedef {import('../types/analyzers').FindClassesConfig} FindClassesConfig */
/**
* @desc Finds import specifiers and sources
* Finds import specifiers and sources
* @param {BabelAst} ast
* @param {string} relativePath the file being currently processed
*/
@ -19,8 +19,9 @@ async function findMembersPerAstEntry(ast, fullCurrentFilePath, projectPath) {
// The transformed entry
const classesFound = [];
/**
* @desc Detects private/publicness based on underscores. Checks '$' as well
* @returns {'public|protected|private'}
* Detects private/publicness based on underscores. Checks '$' as well
* @param {string} name
* @returns {'public'|'protected'|'private'}
*/
function computeAccessType(name) {
if (name.startsWith('_') || name.startsWith('$')) {

View file

@ -7,9 +7,8 @@ const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js');
const { aForEach } = require('../utils/async-array-utils.js');
const { LogService } = require('../services/LogService.js');
/** @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile */
/**
* @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile
* @typedef {object} RootFileMapEntry
* @property {string} currentFileSpecifier this is the local name in the file we track from
* @property {RootFile} rootFile contains file(filePath) and specifier

View file

@ -5,16 +5,23 @@ const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js');
const { Analyzer } = require('./helpers/Analyzer.js');
const { LogService } = require('../services/LogService.js');
/**
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../types/analyzers').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/analyzers').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
* @typedef {import('../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
*/
/**
* Options that allow to filter 'on a file basis'.
* We can also filter on the total result
*/
const /** @type {AnalyzerOptions} */ options = {
/**
* @desc Only leaves entries with external sources:
* Only leaves entries with external sources:
* - keeps: '@open-wc/testing'
* - drops: '../testing'
* @param {FindImportsAnalysisResult} result
* @param {FindImportsAnalyzerResult} result
* @param {string} targetSpecifier for instance 'LitElement'
*/
onlyExternalSources(result) {
@ -41,14 +48,14 @@ function getImportOrReexportsSpecifiers(node) {
}
/**
* @desc Finds import specifiers and sources
* @param {BabelAst} ast
* @param {string} context.relativePath the file being currently processed
* Finds import specifiers and sources
* @param {any} ast
*/
function findImportsPerAstEntry(ast) {
LogService.debug(`Analyzer "find-imports": started findImportsPerAstEntry method`);
// Visit AST...
/** @type {Partial<FindImportsAnalyzerEntry>[]} */
const transformedEntry = [];
traverse(ast, {
ImportDeclaration(path) {
@ -96,11 +103,12 @@ function findImportsPerAstEntry(ast) {
class FindImportsAnalyzer extends Analyzer {
constructor() {
super();
/** @type {AnalyzerName} */
this.name = 'find-imports';
}
/**
* @desc Finds import specifiers and sources
* Finds import specifiers and sources
* @param {FindImportsConfig} customConfig
*/
async execute(customConfig = {}) {

View file

@ -6,31 +6,40 @@ const { LogService } = require('../../services/LogService.js');
const { QueryService } = require('../../services/QueryService.js');
const { ReportService } = require('../../services/ReportService.js');
const { InputDataService } = require('../../services/InputDataService.js');
const { aForEach } = require('../../utils/async-array-utils.js');
const { toPosixPath } = require('../../utils/to-posix-path.js');
const { getFilePathRelativeFromRoot } = require('../../utils/get-file-path-relative-from-root.js');
/**
* @desc analyzes one entry: the callback can traverse a given ast for each entry
* @param {AstDataProject[]} astDataProjects
* @typedef {import('../../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../types/core').QueryOutput} QueryOutput
* @typedef {import('../../types/core').ProjectInputData} ProjectInputData
* @typedef {import('../../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta
* @typedef {import('../../types/core').AnalyzerQueryResult} AnalyzerQueryResult
* @typedef {import('../../types/core').MatchAnalyzerConfig} MatchAnalyzerConfig
*/
/**
* Analyzes one entry: the callback can traverse a given ast for each entry
* @param {ProjectInputDataWithMeta} projectData
* @param {function} astAnalysis
*/
async function analyzePerAstEntry(projectData, astAnalysis) {
const entries = [];
await aForEach(projectData.entries, async ({ file, ast, context: astContext }) => {
for (const { file, ast, context: astContext } of projectData.entries) {
const relativePath = getFilePathRelativeFromRoot(file, projectData.project.path);
const context = { code: astContext.code, relativePath, projectData };
LogService.debug(`${pathLib.resolve(projectData.project.path, file)}`);
const { result, meta } = await astAnalysis(ast, context);
entries.push({ file: relativePath, meta, result });
});
}
const filteredEntries = entries.filter(({ result }) => Boolean(result.length));
return filteredEntries;
}
/**
* Transforms QueryResult entries to posix path notations on Windows
* @param {array|object} data
* @param {object[]|object} data
*/
function posixify(data) {
if (!data) {
@ -55,9 +64,9 @@ function posixify(data) {
* @desc This method ensures that the result returned by an analyzer always has a consistent format.
* By returning the configuration for the queryOutput, it will be possible to run later queries
* under the same circumstances
* @param {array} queryOutput
* @param {QueryOutput} queryOutput
* @param {object} configuration
* @param {object} analyzer
* @param {Analyzer} analyzer
*/
function ensureAnalyzerResultFormat(queryOutput, configuration, analyzer) {
const { targetProjectMeta, identifier, referenceProjectMeta } = analyzer;
@ -71,7 +80,7 @@ function ensureAnalyzerResultFormat(queryOutput, configuration, analyzer) {
delete optional.referenceProject.path; // get rid of machine specific info
}
/** @type {AnalyzerResult} */
/** @type {AnalyzerQueryResult} */
const aResult = {
queryOutput,
analyzerMeta: {
@ -114,11 +123,11 @@ function ensureAnalyzerResultFormat(queryOutput, configuration, analyzer) {
}
/**
* @desc Before running the analyzer, we need two conditions for a 'compatible match':
* 1. referenceProject is imported by targetProject at all
* 2. referenceProject and targetProject have compatible major versions
* @param {string} referencePath
* @param {string} targetPath
* Before running the analyzer, we need two conditions for a 'compatible match':
* - 1. referenceProject is imported by targetProject at all
* - 2. referenceProject and targetProject have compatible major versions
* @param {PathFromSystemRoot} referencePath
* @param {PathFromSystemRoot} targetPath
*/
function checkForMatchCompatibility(referencePath, targetPath) {
const refFile = pathLib.resolve(referencePath, 'package.json');
@ -142,7 +151,7 @@ function checkForMatchCompatibility(referencePath, targetPath) {
/**
* If in json format, 'unwind' to be compatible for analysis...
* @param {AnalyzerResult} targetOrReferenceProjectResult
* @param {AnalyzerQueryResult} targetOrReferenceProjectResult
*/
function unwindJsonResult(targetOrReferenceProjectResult) {
const { queryOutput } = targetOrReferenceProjectResult;
@ -153,6 +162,8 @@ function unwindJsonResult(targetOrReferenceProjectResult) {
class Analyzer {
constructor() {
this.requiredAst = 'babel';
/** @type {AnalyzerName|''} */
this.name = '';
}
static get requiresReference() {
@ -262,7 +273,7 @@ class Analyzer {
/**
* @param {QueryOutput} queryOutput
* @param {AnalyzerConfig} cfg
* @returns {AnalyzerResult}
* @returns {AnalyzerQueryResult}
*/
_finalize(queryOutput, cfg) {
LogService.debug(`Analyzer "${this.name}": started _finalize method`);
@ -319,7 +330,7 @@ class Analyzer {
* @param {object} config
* @param {string} config.analyzerName
* @param {string} config.identifier
* @returns {AnalyzerResult|undefined}
* @returns {AnalyzerQueryResult|undefined}
*/
static _getCachedAnalyzerResult({ analyzerName, identifier }) {
const cachedResult = ReportService.getCachedResult({ analyzerName, identifier });
@ -328,7 +339,7 @@ class Analyzer {
}
LogService.success(`cached version found for ${identifier}`);
/** @type {AnalyzerResult} */
/** @type {AnalyzerQueryResult} */
const result = unwindJsonResult(cachedResult);
result.analyzerMeta.__fromCache = true;
return result;

View file

@ -2,6 +2,10 @@ const { isRelativeSourcePath } = require('../../utils/relative-source-path.js');
const { LogService } = require('../../services/LogService.js');
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
/**
* @typedef {import('../../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
*/
/**
* @param {string} importee like '@lion/core/myFile.js'
* @returns {string} project name ('@lion/core')
@ -31,7 +35,7 @@ function getProjectFromImportee(importee) {
* @param {object} config
* @param {string} config.importee 'reference-project/foo.js'
* @param {string} config.importer '/my/project/importing-file.js'
* @returns {Promise<string|null>} './foo.js'
* @returns {Promise<PathRelativeFromProjectRoot|null>} './foo.js'
*/
async function fromImportToExportPerspective({ importee, importer }) {
if (isRelativeSourcePath(importee)) {
@ -42,9 +46,14 @@ async function fromImportToExportPerspective({ importee, importer }) {
const absolutePath = await resolveImportPath(importee, importer);
const projectName = getProjectFromImportee(importee);
// from /my/reference/project/packages/foo/index.js to './packages/foo/index.js'
/**
* - from: '/my/reference/project/packages/foo/index.js'
* - to: './packages/foo/index.js'
*/
return absolutePath
? absolutePath.replace(new RegExp(`^.*/${projectName}/?(.*)$`), './$1')
? /** @type {PathRelativeFromProjectRoot} */ (
absolutePath.replace(new RegExp(`^.*/${projectName}/?(.*)$`), './$1')
)
: null;
}

View file

@ -1,13 +1,22 @@
/* eslint-disable no-param-reassign */
const pathLib = require('path');
const {
isRelativeSourcePath,
// toRelativeSourcePath,
} = require('../../utils/relative-source-path.js');
const { isRelativeSourcePath } = require('../../utils/relative-source-path.js');
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
const { aMap } = require('../../utils/async-array-utils.js');
const { toPosixPath } = require('../../utils/to-posix-path.js');
const { aMap } = require('../../utils/async-array-utils.js');
/**
* @typedef {import('../../types/core').PathRelative} PathRelative
* @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../../types/core').QueryOutput} QueryOutput
*/
/**
*
* @param {PathFromSystemRoot} currentDirPath
* @param {PathFromSystemRoot} resolvedPath
* @returns {PathRelative}
*/
function toLocalPath(currentDirPath, resolvedPath) {
let relativeSourcePath = pathLib.relative(currentDirPath, resolvedPath);
if (!relativeSourcePath.startsWith('.')) {
@ -16,29 +25,32 @@ function toLocalPath(currentDirPath, resolvedPath) {
// so 'my-local-files.js' -> './my-local-files.js'
relativeSourcePath = `./${relativeSourcePath}`;
}
return toPosixPath(relativeSourcePath);
return /** @type {PathRelative} */ (toPosixPath(relativeSourcePath));
}
/**
* @desc Resolves and converts to normalized local/absolute path, based on file-system information.
* Resolves and converts to normalized local/absolute path, based on file-system information.
* - from: { source: '../../relative/file' }
* - to: {
* fullPath: './absolute/path/from/root/to/relative/file.js',
* normalizedPath: '../../relative/file.js'
* }
* @param {FindImportsAnalysisResult} result
* @param {string} result
* @param {QueryOutput} queryOutput
* @param {string} relativePath
* @returns {string} a relative path from root (usually a project) or an external path like 'lion-based-ui/x.js'
* @param {string} rootPath
*/
async function normalizeSourcePaths(queryOutput, relativePath, rootPath = process.cwd()) {
const currentFilePath = pathLib.resolve(rootPath, relativePath);
const currentDirPath = pathLib.dirname(currentFilePath);
const currentFilePath = /** @type {PathFromSystemRoot} */ (
pathLib.resolve(rootPath, relativePath)
);
const currentDirPath = /** @type {PathFromSystemRoot} */ (pathLib.dirname(currentFilePath));
return aMap(queryOutput, async specifierResObj => {
if (specifierResObj.source) {
if (isRelativeSourcePath(specifierResObj.source) && relativePath) {
// This will be a source like '../my/file.js' or './file.js'
const resolvedPath = await resolveImportPath(specifierResObj.source, currentFilePath);
const resolvedPath = /** @type {PathFromSystemRoot} */ (
await resolveImportPath(specifierResObj.source, currentFilePath)
);
specifierResObj.normalizedSource =
resolvedPath && toLocalPath(currentDirPath, resolvedPath);
// specifierResObj.fullSource = resolvedPath && toRelativeSourcePath(resolvedPath, rootPath);

View file

@ -9,14 +9,19 @@ const { AstService } = require('../../services/AstService.js');
const { LogService } = require('../../services/LogService.js');
const { memoizeAsync } = require('../../utils/memoize.js');
/** @typedef {import('./types').RootFile} RootFile */
/**
* @typedef {import('../../types/core').RootFile} RootFile
* @typedef {import('../../types/core').SpecifierSource} SpecifierSource
* @typedef {import('../../types/core').IdentifierName} IdentifierName
* @typedef {import('../../types/core').PathFromSystemRoot} PathFromSystemRoot
*/
/**
* Other than with import, no binding is created for MyClass by Babel(?)
* This means 'path.scope.getBinding('MyClass')' returns undefined
* and we have to find a different way to retrieve this value.
* @param {object} astPath Babel ast traversal path
* @param {string} identifierName the name that should be tracked (and that exists inside scope of astPath)
* @param {IdentifierName} identifierName the name that should be tracked (and that exists inside scope of astPath)
*/
function getBindingAndSourceReexports(astPath, identifierName) {
// Get to root node of file and look for exports like `export { identifierName } from 'src';`
@ -81,6 +86,7 @@ function getImportSourceFromAst(astPath, identifierName) {
return { source, importedIdentifierName };
}
/** @type {(source:SpecifierSource,identifierName:IdentifierName,currentFilePath:PathFromSystemRoot,rootPath:PathFromSystemRoot, depth?:number) => Promise<RootFile>} */
let trackDownIdentifier;
/**
* @example
@ -98,11 +104,11 @@ let trackDownIdentifier;
* export class RefComp extends LitElement {...}
*```
*
* @param {string} source an importSpecifier source, like 'ref-proj' or '../file'
* @param {string} identifierName imported reference/Identifier name, like 'MyComp'
* @param {string} currentFilePath file path, like '/path/to/target-proj/my-comp-import.js'
* @param {string} rootPath dir path, like '/path/to/target-proj'
* @returns {object} file: path of file containing the binding (exported declaration),
* @param {SpecifierSource} source an importSpecifier source, like 'ref-proj' or '../file'
* @param {IdentifierName} identifierName imported reference/Identifier name, like 'MyComp'
* @param {PathFromSystemRoot} currentFilePath file path, like '/path/to/target-proj/my-comp-import.js'
* @param {PathFromSystemRoot} rootPath dir path, like '/path/to/target-proj'
* @returns {Promise<RootFile>} file: path of file containing the binding (exported declaration),
* like '/path/to/ref-proj/src/RefComp.js'
*/
async function trackDownIdentifierFn(source, identifierName, currentFilePath, rootPath, depth = 0) {
@ -122,9 +128,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
}
/**
* @prop resolvedSourcePath
* @type {string}
* @example resolveImportPath('../file') // => '/path/to/target-proj/file.js'
* @type {PathFromSystemRoot}
*/
const resolvedSourcePath = await resolveImportPath(source, currentFilePath);
LogService.debug(`[trackDownIdentifier] ${resolvedSourcePath}`);
@ -132,7 +136,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
const ast = AstService.getAst(code, 'babel', { filePath: resolvedSourcePath });
const shouldLookForDefaultExport = identifierName === '[default]';
let reexportMatch = null; // named specifier declaration
let reexportMatch = false; // named specifier declaration
let pendingTrackDownPromise;
traverse(ast, {

View file

@ -7,19 +7,22 @@ const { Analyzer } = require('./helpers/Analyzer.js');
const { fromImportToExportPerspective } = require('./helpers/from-import-to-export-perspective.js');
/**
* @typedef {import('../types/find-imports').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/find-exports').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../types/find-exports').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @typedef {import('../types/find-imports').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
* @typedef {import('../types/match-imports').ConciseMatchImportsAnalyzerResult} ConciseMatchImportsAnalyzerResult
* @typedef {import('../types/core').PathRelativeFromRoot} PathRelativeFromRoot
* @typedef {import('../types/analyzers').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/analyzers').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../types/analyzers').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @typedef {import('../types/analyzers').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
* @typedef {import('../types/analyzers').ConciseMatchImportsAnalyzerResult} ConciseMatchImportsAnalyzerResult
* @typedef {import('../types/analyzers').MatchImportsConfig} MatchImportsConfig
* @typedef {import('../types/analyzers').MatchImportsAnalyzerResult} MatchImportsAnalyzerResult
* @typedef {import('../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
*/
/**
* Needed in case fromImportToExportPerspective does not have a
* externalRootPath supplied.
* @param {string} exportPath exportEntry.file
* @param {string} translatedImportPath result of fromImportToExportPerspective
* @param {PathRelativeFromProjectRoot} translatedImportPath result of fromImportToExportPerspective
*/
function compareImportAndExportPaths(exportPath, translatedImportPath) {
return (
@ -69,7 +72,7 @@ function transformIntoIterableFindExportsOutput(exportsAnalyzerResult) {
for (const { file, result } of exportsAnalyzerResult.queryOutput) {
for (const { exportSpecifiers, source, rootFileMap, localMap, meta } of result) {
if (!exportSpecifiers) {
break;
continue;
}
for (const exportSpecifier of exportSpecifiers) {
const i = exportSpecifiers.indexOf(exportSpecifier);
@ -126,7 +129,7 @@ function transformIntoIterableFindImportsOutput(importsAnalyzerResult) {
for (const { file, result } of importsAnalyzerResult.queryOutput) {
for (const { importSpecifiers, source, normalizedSource } of result) {
if (!importSpecifiers) {
break;
continue;
}
for (const importSpecifier of importSpecifiers) {
/** @type {IterableFindImportsAnalyzerEntry} */
@ -144,8 +147,9 @@ function transformIntoIterableFindImportsOutput(importsAnalyzerResult) {
}
/**
* Makes a concise results array a 'compatible resultsArray' (compatible with dashbaord + tests + ...?)
* @param {object[]} conciseResultsArray
* Makes a 'compatible resultsArray' (compatible with dashboard + tests + ...?) from
* a conciseResultsArray.
* @param {ConciseMatchImportsAnalyzerResult} conciseResultsArray
* @param {string} importProject
*/
function createCompatibleMatchImportsResult(conciseResultsArray, importProject) {
@ -169,8 +173,8 @@ function createCompatibleMatchImportsResult(conciseResultsArray, importProject)
/**
* @param {FindExportsAnalyzerResult} exportsAnalyzerResult
* @param {FindImportsAnalyzerResult} importsAnalyzerResult
* @param {matchImportsConfig} customConfig
* @returns {Promise<AnalyzerResult>}
* @param {MatchImportsConfig} customConfig
* @returns {Promise<MatchImportsAnalyzerResult>}
*/
async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) {
const cfg = {
@ -217,7 +221,7 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes
* => export const z = 'bar'
* importFile 'importing-target-project/file.js'
* => import { z } from '@reference/foo.js'
* @type {PathRelativeFromRoot}
* @type {PathRelativeFromProjectRoot|null}
*/
const fromImportToExport = await fromImportToExportPerspective({
importee: importEntry.normalizedSource,
@ -246,7 +250,7 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes
}
const importProject = importsAnalyzerResult.analyzerMeta.targetProject.name;
return /** @type {AnalyzerResult} */ createCompatibleMatchImportsResult(
return /** @type {AnalyzerQueryResult} */ createCompatibleMatchImportsResult(
conciseResultsArray,
importProject,
);
@ -255,6 +259,7 @@ async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerRes
class MatchImportsAnalyzer extends Analyzer {
constructor() {
super();
/** @type {AnalyzerName} */
this.name = 'match-imports';
}
@ -263,7 +268,7 @@ class MatchImportsAnalyzer extends Analyzer {
}
/**
* @desc Based on ExportsAnalyzerResult of reference project(s) (for instance lion-based-ui)
* 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

View file

@ -258,7 +258,7 @@ function getTagPaths(
* @param {FindCustomelementsAnalyzerResult} targetFindCustomelementsResult
* @param {FindCustomelementsAnalyzerResult} refFindCustomelementsResult
* @param {FindExportsAnalyzerResult} refFindExportsResult
* @returns {AnalyzerResult}
* @returns {AnalyzerQueryResult}
*/
function matchPathsPostprocess(
targetMatchSubclassesResult,
@ -268,7 +268,7 @@ function matchPathsPostprocess(
refFindExportsResult,
refProjectName,
) {
/** @type {AnalyzerResult} */
/** @type {AnalyzerQueryResult} */
const resultsArray = [];
targetMatchSubclassesResult.queryOutput.forEach(matchSubclassEntry => {

View file

@ -6,8 +6,16 @@ const FindExportsAnalyzer = require('./find-exports.js');
const { Analyzer } = require('./helpers/Analyzer.js');
const { fromImportToExportPerspective } = require('./helpers/from-import-to-export-perspective.js');
/** @typedef {import('./types').FindClassesAnalyzerResult} FindClassesAnalyzerResult */
/** @typedef {import('./types').FindExportsAnalyzerResult} FindExportsAnalyzerResult */
/**
* @typedef {import('../types/analyzers/find-classes').FindClassesAnalyzerResult} FindClassesAnalyzerResult
* @typedef {import('../types/find-imports').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/find-exports').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../types/find-exports').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @typedef {import('../types/find-imports').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
* @typedef {import('../types/match-imports').ConciseMatchImportsAnalyzerResult} ConciseMatchImportsAnalyzerResult
* @typedef {import('../types/match-imports').MatchImportsConfig} MatchImportsConfig
* @typedef {import('../types/core/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
*/
function getMemberOverrides(
refClassesAResult,
@ -63,7 +71,7 @@ function storeResult(resultsObj, exportId, filteredList, meta) {
* @param {FindClassesAnalyzerResult} targetClassesAnalyzerResult
* @param {FindClassesAnalyzerResult} refClassesAResult
* @param {MatchSubclassesConfig} customConfig
* @returns {AnalyzerResult}
* @returns {AnalyzerQueryResult}
*/
async function matchSubclassesPostprocess(
exportsAnalyzerResult,
@ -243,7 +251,7 @@ async function matchSubclassesPostprocess(
})
.filter(r => Object.keys(r.matchesPerProject).length);
return /** @type {AnalyzerResult} */ resultsArray;
return /** @type {AnalyzerQueryResult} */ resultsArray;
}
// function postProcessAnalyzerResult(aResult) {

View file

@ -9,9 +9,9 @@ const /** @type {AnalyzerOptions} */ options = {
/**
*
* @param {AnalyzerResult} analyzerResult
* @param {AnalyzerQueryResult} analyzerResult
* @param {FindImportsConfig} customConfig
* @returns {AnalyzerResult}
* @returns {AnalyzerQueryResult}
*/
function sortBySpecifier(analyzerResult, customConfig) {
const cfg = {
@ -74,7 +74,7 @@ function sortBySpecifier(analyzerResult, customConfig) {
);
}
return /** @type {AnalyzerResult} */ resultsBySpecifier;
return /** @type {AnalyzerQueryResult} */ resultsBySpecifier;
}
module.exports = {

View file

@ -1,298 +0,0 @@
import { ProjectReference } from 'typescript';
export interface RootFile {
/** the file path containing declaration, for instance './target-src/direct-imports.js'. Can also contain keyword '[current]' */
file: string;
/** the specifier/identifier that was exported in root file, for instance 'MyClass' */
specifier: string;
}
export interface AnalyzerResult {
/** meta info object */
meta: Meta;
/** array of AST traversal output, per project file */
queryOutput: AnalyzerOutputFile[];
}
export interface AnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: string;
/** result of AST traversal for file in project */
result: array;
}
// TODO: make sure that data structures of JSON output (generated in ReportService)
// and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize)
// so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema
interface Meta {
/** type of the query. Currently onlu "ast-analyzer" supported */
searchType: string;
/** analyzer meta object */
analyzerMeta: AnalyzerMeta;
}
export interface AnalyzerMeta {
/** analizer name like 'find-imports' or 'match-sublcasses' */
name: string;
/** the ast format. Currently only 'babel' */
requiredAst: string;
/** a unique hash based on target, reference and configuration */
identifier: string;
/** target project meta object */
targetProject: Project;
/** reference project meta object */
referenceProject?: Project;
/** the configuration used for this particular analyzer run */
configuration: object;
}
export interface Project {
/** "name" found in package.json and under which the package is registered in npm */
name: string;
/** "version" found in package.json */
version: string;
/** "main" File found in package.json */
mainFile: string;
/** if a git repo is analyzed, stores commit hash, [not-a-git-repo] if not */
commitHash: string;
}
// match-customelements
export interface MatchSubclassesAnalyzerResult extends AnalyzerResult {
queryOutput: MatchSubclassesAnalyzerOutputEntry[];
}
export interface MatchSubclassesAnalyzerOutputEntry {
exportSpecifier: MatchedExportSpecifier;
matchesPerProject: MatchSubclassesAnalyzerOutputEntryMatch[];
}
export interface MatchSubclassesAnalyzerOutputEntryMatch {
/** The target project that extends the class exported by reference project */
project: string;
/** Array of meta objects for matching files */
files: MatchSubclassesAnalyzerOutputEntryMatchFile[];
}
export interface MatchSubclassesAnalyzerOutputEntryMatchFile {
/**
* The local filepath that contains the matched class inside the target project
* like `./src/ExtendedClass.js`
*/
file: string;
/**
* The local Identifier inside matched file that is exported
* @example
* - `ExtendedClass` for `export ExtendedClass extends RefClass {};`
* - `[default]` for `export default ExtendedClass extends RefClass {};`
*/
identifier: string;
}
export interface MatchedExportSpecifier extends AnalyzerResult {
/** The exported Identifier name.
*
* For instance
* - `export { X as Y } from 'q'` => `Y`
* - `export default class Z {}` => `[default]`
*/
name: string;
/** Project name as found in package.json */
project: string;
/** Path relative from project root, for instance `./index.js` */
filePath: string;
/** "[default]::./index.js::exporting-ref-project" */
id: string;
}
// "find-customelements"
export interface FindCustomelementsAnalyzerResult extends AnalyzerResult {
queryOutput: FindCustomelementsAnalyzerOutputFile[];
}
export interface FindCustomelementsAnalyzerOutputFile extends AnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: string;
/** result of AST traversal for file in project */
result: FindCustomelementsAnalyzerEntry[];
}
export interface FindCustomelementsAnalyzerEntry {
/**
* Tag name found in CE definition:
* `customElements.define('my-name', MyConstructor)` => 'my-name'
*/
tagName: string;
/**
* Identifier found in CE definition:
* `customElements.define('my-name', MyConstructor)` => MyConstructor
*/
constructorIdentifier: string;
/** Rootfile traced for constuctorIdentifier found in CE definition */
rootFile: RootFile;
}
// "find-exports"
export interface FindExportsAnalyzerResult extends AnalyzerResult {
queryOutput: FindExportsAnalyzerOutputFile[];
}
export interface FindExportsAnalyzerOutputFile extends AnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: string;
/** result of AST traversal for file in project */
result: FindExportsAnalyzerEntry[];
}
export interface FindExportsAnalyzerEntry {
/**
* The specifiers found in an export statement.
*
* For example:
* - file `export class X {}` gives `['X']`
* - file `export default const y = 0` gives `['[default]']`
* - file `export { y, z } from 'project'` gives `['y', 'z']`
*/
exportSpecifiers: string[];
/**
* The original "source" string belonging to specifier.
* For example:
* - file `export { x } from './my/file';` gives `"./my/file"`
* - file `export { x } from 'project';` gives `"project"`
*/
source: string;
/**
* The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions).
* For example:
* - file `export { x } from './my/file';` gives `"./my/file.js"`
* - file `export { x } from 'project';` gives `"project"` (only files in current project are resolved)
* - file `export { x } from '../';` gives `"../index.js"`
*/
normalizedSource: string;
/** map of tracked down Identifiers */
rootFileMap: RootFileMapEntry[];
}
export interface RootFileMapEntry {
/** This is the local name in the file we track from */
currentFileSpecifier: string;
/**
* The file that contains the original declaration of a certain Identifier/Specifier.
* Contains file(filePath) and specifier keys
*/
rootFile: RootFile;
}
// "find-imports"
export interface FindImportsAnalyzerResult extends AnalyzerResult {
queryOutput: FindImportsAnalyzerOutputFile[];
}
export interface FindImportsAnalyzerOutputFile extends AnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: string;
/** result of AST traversal for file in project */
result: FindImportsAnalyzerEntry[];
}
export interface FindImportsAnalyzerEntry {
/**
* The specifiers found in an import statement.
*
* For example:
* - file `import { X } from 'project'` gives `['X']`
* - file `import X from 'project'` gives `['[default]']`
* - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']`
*/
importSpecifiers: string[];
/**
* The original "source" string belonging to specifier.
* For example:
* - file `import { x } from './my/file';` gives `"./my/file"`
* - file `import { x } from 'project';` gives `"project"`
*/
source: string;
/**
* The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions).
* For example:
* - file `import { x } from './my/file';` gives `"./my/file.js"`
* - file `import { x } from 'project';` gives `"project"` (only files in current project are resolved)
* - file `import { x } from '../';` gives `"../index.js"`
*/
normalizedSource: string;
}
// "find-classes"
export interface FindClassesAnalyzerResult extends AnalyzerResult {
queryOutput: FindClassesAnalyzerOutputFile[];
}
export interface FindClassesAnalyzerOutputFile extends AnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: string;
/** result of AST traversal for file in project */
result: FindClassesAnalyzerEntry[];
}
export interface FindClassesAnalyzerEntry {
/** the name of the class */
name: string;
/** whether the class is a mixin function */
isMixin: boolean;
/** super classes and mixins */
superClasses: SuperClass[];
members: ClassMember;
}
interface ClassMember {
props: ClassProperty;
methods: ClassMethod;
}
interface ClassProperty {
/** class property name */
name: string;
/** 'public', 'protected' or 'private' */
accessType: string;
/** can be 'get', 'set' or both */
kind: Array;
/** whether property is static */
static: boolean;
}
interface ClassMethod {
/** class method name */
name: string;
/** 'public', 'protected' or 'private' */
accessType: boolean;
}
export interface SuperClass {
/** the name of the super class */
name: string;
/** whether the superClass is a mixin function */
isMixin: boolean;
rootFile: RootFile;
}
export interface FindClassesConfig {
/** search target paths */
targetProjectPath: string;
}
export interface AnalyzerConfig {
/** search target project path */
targetProjectPath: string;
gatherFilesConfig: GatherFilesConfig;
}
export interface MatchAnalyzerConfig extends AnalyzerConfig {
/** reference project path, used to match reference against target */
referenceProjectPath: string;
}

View file

@ -34,7 +34,7 @@ function report(queryResult, cfg) {
}
/**
* @desc creates unique QueryConfig for analyzer turn
* Creates unique QueryConfig for analyzer turn
* @param {QueryConfig} queryConfig
* @param {string} targetProjectPath
* @param {string} referenceProjectPath
@ -194,7 +194,7 @@ async function providenceMain(queryConfig, customConfig) {
}
let queryResults;
if (queryConfig.type === 'analyzer') {
if (queryConfig.type === 'ast-analyzer') {
queryResults = await handleAnalyzer(queryConfig, cfg);
} else {
const inputData = InputDataService.createDataObject(

View file

@ -1,3 +1,4 @@
// @ts-nocheck
const {
createProgram,
getPreEmitDiagnostics,
@ -6,17 +7,22 @@ const {
ScriptTarget,
} = require('typescript');
const babelParser = require('@babel/parser');
// @ts-expect-error
const esModuleLexer = require('es-module-lexer');
const parse5 = require('parse5');
const traverseHtml = require('../utils/traverse-html.js');
const { LogService } = require('./LogService.js');
/**
* @typedef {import('../types/core').PathFromSystemRoot} PathFromSystemRoot
*/
class AstService {
/**
* @deprecated for simplicity/maintainability, only allow Babel for js
* Compiles an array of file paths using Typescript.
* @param {string[]} filePaths
* @param options
* @param {CompilerOptions} options
*/
static _getTypescriptAst(filePaths, options) {
// eslint-disable-next-line no-param-reassign
@ -51,7 +57,6 @@ class AstService {
/**
* Compiles an array of file paths using Babel.
* @param {string} code
* @param {object} [options]
*/
static _getBabelAst(code) {
const ast = babelParser.parse(code, {
@ -62,7 +67,7 @@ class AstService {
}
/**
* @desc Combines all script tags as if it were one js file.
* Combines all script tags as if it were one js file.
* @param {string} htmlCode
*/
static getScriptsFromHtml(htmlCode) {
@ -86,7 +91,7 @@ class AstService {
}
/**
* @desc Returns the desired AST
* Returns the desired AST
* Why would we support multiple ASTs/parsers?
* - 'babel' is our default tool for analysis. It's the most versatile and popular tool, it's
* close to the EStree standard (other than Typescript) and a lot of plugins and resources can
@ -95,8 +100,7 @@ class AstService {
* - 'es-module-lexer' (deprecated) is needed for the dedicated task of finding module imports; it is way
* quicker than a full fledged AST parser
* @param { 'babel' } astType
* @param { object } [options]
* @param { string } [options.filePath] the path of the file we're trying to parse
* @param { {filePath: PathFromSystemRoot} } [options]
*/
// eslint-disable-next-line consistent-return
static getAst(code, astType, { filePath } = {}) {

View file

@ -1,20 +1,49 @@
/* eslint-disable no-param-reassign */
// @ts-ignore-next-line
require('../types/index.js');
const fs = require('fs');
const pathLib = require('path');
const child_process = require('child_process'); // eslint-disable-line camelcase
const glob = require('glob');
const anymatch = require('anymatch');
// @ts-expect-error
const isNegatedGlob = require('is-negated-glob');
const { LogService } = require('./LogService.js');
const { AstService } = require('./AstService.js');
const { getFilePathRelativeFromRoot } = require('../utils/get-file-path-relative-from-root.js');
const { toPosixPath } = require('../utils/to-posix-path.js');
/**
* @typedef {import('../types/analyzers').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/analyzers').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
* @typedef {import('../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../types/core').QueryConfig} QueryConfig
* @typedef {import('../types/core').QueryResult} QueryResult
* @typedef {import('../types/core').FeatureQueryConfig} FeatureQueryConfig
* @typedef {import('../types/core').SearchQueryConfig} SearchQueryConfig
* @typedef {import('../types/core').AnalyzerQueryConfig} AnalyzerQueryConfig
* @typedef {import('../types/core').Feature} Feature
* @typedef {import('../types/core').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../types/core').Analyzer} Analyzer
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../types/core').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../types/core').GatherFilesConfig} GatherFilesConfig
* @typedef {import('../types/core').AnalyzerQueryResult} AnalyzerQueryResult
* @typedef {import('../types/core').ProjectInputData} ProjectInputData
* @typedef {import('../types/core').ProjectInputDataWithMeta} ProjectInputDataWithMeta
* @typedef {import('../types/core').Project} Project
* @typedef {import('../types/core').ProjectName} ProjectName
*/
/**
* @typedef {{path:PathFromSystemRoot; name:ProjectName}} ProjectNameAndPath
* @typedef {{name:ProjectName;files:PathRelativeFromProjectRoot[], workspaces:string[]}} PkgJson
*/
// TODO: memoize
/**
* @param {PathFromSystemRoot} rootPath
* @returns {PkgJson|undefined}
*/
function getPackageJson(rootPath) {
try {
const fileContent = fs.readFileSync(`${rootPath}/package.json`, 'utf8');
@ -24,6 +53,9 @@ function getPackageJson(rootPath) {
}
}
/**
* @param {PathFromSystemRoot} rootPath
*/
function getLernaJson(rootPath) {
try {
const fileContent = fs.readFileSync(`${rootPath}/lerna.json`, 'utf8');
@ -35,11 +67,12 @@ function getLernaJson(rootPath) {
/**
*
* @param {string[]} list
* @param {string} rootPath
* @returns {{path:string, name:string}[]}
* @param {PathFromSystemRoot[]|string[]} list
* @param {PathFromSystemRoot} rootPath
* @returns {ProjectNameAndPath[]}
*/
function getPathsFromGlobList(list, rootPath) {
/** @type {string[]} */
const results = [];
list.forEach(pathOrGlob => {
if (!pathOrGlob.endsWith('/')) {
@ -56,15 +89,19 @@ function getPathsFromGlobList(list, rootPath) {
results.push(pathOrGlob);
}
});
return results.map(path => {
const packageRoot = pathLib.resolve(rootPath, path);
const basename = pathLib.basename(path);
const pkgJson = getPackageJson(packageRoot);
const name = (pkgJson && pkgJson.name) || basename;
return { name, path };
return results.map(pkgPath => {
const packageRoot = pathLib.resolve(rootPath, pkgPath);
const basename = pathLib.basename(pkgPath);
const pkgJson = getPackageJson(/** @type {PathFromSystemRoot} */ (packageRoot));
const name = /** @type {ProjectName} */ ((pkgJson && pkgJson.name) || basename);
return { name, path: /** @type {PathFromSystemRoot} */ (pkgPath) };
});
}
/**
* @param {PathFromSystemRoot} rootPath
* @returns {string|undefined}
*/
function getGitignoreFile(rootPath) {
try {
return fs.readFileSync(`${rootPath}/.gitignore`, 'utf8');
@ -73,6 +110,10 @@ function getGitignoreFile(rootPath) {
}
}
/**
* @param {PathFromSystemRoot} rootPath
* @returns {string[]}
*/
function getGitIgnorePaths(rootPath) {
const fileContent = getGitignoreFile(rootPath);
if (!fileContent) {
@ -91,8 +132,8 @@ function getGitIgnorePaths(rootPath) {
});
// normalize entries to be compatible with anymatch
const normalizedEntries = entries.map(e => {
let entry = toPosixPath(e);
const normalizedEntries = entries.map(entry => {
entry = toPosixPath(entry);
if (entry.startsWith('/')) {
entry = entry.slice(1);
@ -110,6 +151,8 @@ function getGitIgnorePaths(rootPath) {
/**
* Gives back all files and folders that need to be added to npm artifact
* @param {PathFromSystemRoot} rootPath
* @returns {string[]}
*/
function getNpmPackagePaths(rootPath) {
const pkgJson = getPackageJson(rootPath);
@ -129,14 +172,17 @@ function getNpmPackagePaths(rootPath) {
}
/**
*
* @param {string|array} v
* @returns {array}
* @param {any|any[]} v
* @returns {any[]}
*/
function ensureArray(v) {
return Array.isArray(v) ? v : [v];
}
/**
* @param {string|string[]} patterns
* @param {Partial<{keepDirs:boolean;root:string}>} [options]
*/
function multiGlobSync(patterns, { keepDirs = false, root } = {}) {
patterns = ensureArray(patterns);
const res = new Set();
@ -161,43 +207,48 @@ function multiGlobSync(patterns, { keepDirs = false, root } = {}) {
*/
class InputDataService {
/**
* @desc create an array of ProjectData
* @param {string[]} projectPaths
* @param {GatherFilesConfig} gatherFilesConfig
* @returns {ProjectData}
* Create an array of ProjectData
* @param {PathFromSystemRoot[]} projectPaths
* @param {Partial<GatherFilesConfig>} gatherFilesConfig
* @returns {ProjectInputData[]}
*/
static createDataObject(projectPaths, gatherFilesConfig = {}) {
/** @type {ProjectInputData[]} */
const inputData = projectPaths.map(projectPath => ({
project: {
project: /** @type {Project} */ ({
name: pathLib.basename(projectPath),
path: projectPath,
},
}),
entries: this.gatherFilesFromDir(projectPath, {
...this.defaultGatherFilesConfig,
...gatherFilesConfig,
}),
}));
// @ts-ignore
return this._addMetaToProjectsData(inputData);
}
/**
* From 'main/file.js' or '/main/file.js' to './main/file.js'
* @param {string} mainEntry
* @returns {PathRelativeFromProjectRoot}
*/
static __normalizeMainEntry(mainEntry) {
if (mainEntry.startsWith('/')) {
return `.${mainEntry}`;
return /** @type {PathRelativeFromProjectRoot} */ (`.${mainEntry}`);
}
if (!mainEntry.startsWith('.')) {
return `./${mainEntry}`;
}
return mainEntry;
return /** @type {PathRelativeFromProjectRoot} */ (mainEntry);
}
/**
* @param {string} projectPath
* @returns { { path:string, name?:string, mainEntry?:string, version?: string, commitHash?:string }}
* @param {PathFromSystemRoot} projectPath
* @returns {Project}
*/
static getProjectMeta(projectPath) {
/** @type {Partial<Project>} */
const project = { path: projectPath };
// Add project meta info
try {
@ -212,12 +263,16 @@ class InputDataService {
// eslint-disable-next-line no-empty
project.version = pkgJson.version;
} catch (e) {
LogService.warn(e);
LogService.warn(/** @type {string} */ (e));
}
project.commitHash = this._getCommitHash(projectPath);
return project;
return /** @type {Project} */ (project);
}
/**
* @param {PathFromSystemRoot} projectPath
* @returns {string|'[not-a-git-root]'|undefined}
*/
static _getCommitHash(projectPath) {
let commitHash;
let isGitRepo;
@ -237,7 +292,7 @@ class InputDataService {
// eslint-disable-next-line no-param-reassign
commitHash = hash;
} catch (e) {
LogService.warn(e);
LogService.warn(/** @type {string} */ (e));
}
} else {
commitHash = '[not-a-git-root]';
@ -246,42 +301,49 @@ class InputDataService {
}
/**
* @desc adds context with code (c.q. file contents), project name and project 'main' entry
* @param {InputData} inputData
* Adds context with code (c.q. file contents), project name and project 'main' entry
* @param {ProjectInputData[]} inputData
* @returns {ProjectInputDataWithMeta[]}
*/
static _addMetaToProjectsData(inputData) {
return inputData.map(projectObj => {
// Add context obj with 'code' to files
const newEntries = [];
projectObj.entries.forEach(entry => {
const code = fs.readFileSync(entry, 'utf8');
const file = getFilePathRelativeFromRoot(
toPosixPath(entry),
toPosixPath(projectObj.project.path),
);
if (pathLib.extname(file) === '.html') {
const extractedScripts = AstService.getScriptsFromHtml(code);
// eslint-disable-next-line no-shadow
extractedScripts.forEach((code, i) => {
newEntries.push({ file: `${file}#${i}`, context: { code } });
});
} else {
newEntries.push({ file, context: { code } });
}
});
return /** @type {* & ProjectInputDataWithMeta[]} */ (
inputData.map(projectObj => {
// Add context obj with 'code' to files
const project = this.getProjectMeta(toPosixPath(projectObj.project.path));
/** @type {ProjectInputDataWithMeta['entries'][]} */
const newEntries = [];
projectObj.entries.forEach(entry => {
const code = fs.readFileSync(entry, 'utf8');
const file = getFilePathRelativeFromRoot(
toPosixPath(entry),
toPosixPath(projectObj.project.path),
);
if (pathLib.extname(file) === '.html') {
const extractedScripts = AstService.getScriptsFromHtml(code);
// eslint-disable-next-line no-shadow
extractedScripts.forEach((code, i) => {
newEntries.push({
file: /** @type {PathRelativeFromProjectRoot} */ (`${file}#${i}`),
context: { code },
});
});
} else {
newEntries.push({ file, context: { code } });
}
});
return { project, entries: newEntries };
});
const project = this.getProjectMeta(toPosixPath(projectObj.project.path));
return { project, entries: newEntries };
})
);
}
// TODO: rename to `get targetProjectPaths`
/**
* @desc gets all project directories/paths from './submodules'
* @returns {string[]} a list of strings representing all entry paths for projects we want to query
* Gets all project directories/paths from './submodules'
* @type {PathFromSystemRoot[]} a list of strings representing all entry paths for projects we want to query
*/
static getTargetProjectPaths() {
static get targetProjectPaths() {
if (this.__targetProjectPaths) {
return this.__targetProjectPaths;
}
@ -296,10 +358,13 @@ class InputDataService {
return [];
}
return dirs
.map(dir => pathLib.join(submoduleDir, dir))
.map(dir => /** @type {PathFromSystemRoot} */ (pathLib.join(submoduleDir, dir)))
.filter(dirPath => fs.lstatSync(dirPath).isDirectory());
}
/**
* @type {PathFromSystemRoot[]} a list of strings representing all entry paths for projects we want to query
*/
static get referenceProjectPaths() {
if (this.__referenceProjectPaths) {
return this.__referenceProjectPaths;
@ -314,7 +379,7 @@ class InputDataService {
.filter(dirPath => fs.lstatSync(dirPath).isDirectory());
// eslint-disable-next-line no-empty
} catch (_) {}
return dirs;
return /** @type {PathFromSystemRoot[]} */ (dirs);
}
static set referenceProjectPaths(v) {
@ -325,6 +390,9 @@ class InputDataService {
this.__targetProjectPaths = ensureArray(v);
}
/**
* @type {GatherFilesConfig}
*/
static get defaultGatherFilesConfig() {
return {
extensions: ['.js'],
@ -333,32 +401,32 @@ class InputDataService {
};
}
/**
* @param {PathFromSystemRoot} startPath
* @param {GatherFilesConfig} cfg
* @param {boolean} withoutDepth
*/
static getGlobPattern(startPath, cfg, withoutDepth = false) {
// if startPath ends with '/', remove
let globPattern = startPath.replace(/\/$/, '');
// let globPattern = '';
if (process.platform === 'win32') {
// root = root.replace(/^.\:/, '').replace(/\\/g, '/');
globPattern = globPattern.replace(/^.:/, '').replace(/\\/g, '/');
}
if (!withoutDepth) {
if (cfg.depth !== Infinity) {
if (typeof cfg.depth === 'number' && cfg.depth !== Infinity) {
globPattern += `/*`.repeat(cfg.depth + 1);
} else {
globPattern += `/**/*`;
}
}
// globPattern = globPattern.slice(1)
return { globPattern };
}
/**
* @desc Gets an array of files for given extension
* @param {string} startPath - local filesystem path
* @param {GatherFilesConfig} customConfig - configuration object
* @param {number} [customConfig.depth=Infinity] how many recursive calls should be made
* @param {string[]} [result] - list of file paths, for internal (recursive) calls
* @returns {string[]} result list of file paths
* Gets an array of files for given extension
* @param {PathFromSystemRoot} startPath - local filesystem path
* @param {Partial<GatherFilesConfig>} customConfig - configuration object
* @returns {PathFromSystemRoot[]} result list of file paths
*/
static gatherFilesFromDir(startPath, customConfig = {}) {
const cfg = {
@ -380,7 +448,9 @@ class InputDataService {
);
}
/** @type {string[]} */
let gitIgnorePaths = [];
/** @type {string[]} */
let npmPackagePaths = [];
const hasGitIgnore = getGitignoreFile(startPath);
@ -411,15 +481,19 @@ class InputDataService {
if (removeFilter.length || keepFilter.length) {
filteredGlobRes = globRes.filter(filePath => {
const localFilePath = toPosixPath(filePath).replace(`${toPosixPath(startPath)}/`, '');
// @ts-expect-error
let shouldRemove = removeFilter.length && anymatch(removeFilter, localFilePath);
// @ts-expect-error
let shouldKeep = keepFilter.length && anymatch(keepFilter, localFilePath);
if (shouldRemove && shouldKeep) {
// Contradicting configs: the one defined by end user takes highest precedence
// If the match came from allowListMode, it loses.
// @ts-expect-error
if (allowlistMode === 'git' && anymatch(gitIgnorePaths, localFilePath)) {
// shouldRemove was caused by .gitignore, shouldKeep by custom allowlist
shouldRemove = false;
// @ts-expect-error
} else if (allowlistMode === 'npm' && anymatch(npmPackagePaths, localFilePath)) {
// shouldKeep was caused by npm "files", shouldRemove by custom allowlist
shouldKeep = false;
@ -438,15 +512,17 @@ class InputDataService {
if (!filteredGlobRes || !filteredGlobRes.length) {
LogService.warn(`No files found for path '${startPath}'`);
return [];
}
// reappend startPath
// const res = filteredGlobRes.map(f => pathLib.resolve(startPath, f));
return filteredGlobRes.map(toPosixPath);
return /** @type {PathFromSystemRoot[]} */ (filteredGlobRes.map(toPosixPath));
}
// TODO: use modern web config helper
/**
* @desc Allows the user to provide a providence.conf.js file in its repository root
* Allows the user to provide a providence.conf.js file in its repository root
*/
static getExternalConfig() {
throw new Error(
@ -456,6 +532,8 @@ class InputDataService {
/**
* Gives back all monorepo package paths
* @param {PathFromSystemRoot} rootPath
* @returns {ProjectNameAndPath[]|undefined}
*/
static getMonoRepoPackages(rootPath) {
// [1] Look for npm/yarn workspaces

View file

@ -3,44 +3,85 @@ const chalk = require('chalk');
const ora = require('ora');
const fs = require('fs');
/**
* @typedef {import('ora').Ora} Ora
*/
const { log } = console;
/**
* @param {string} [title]
* @returns {string}
*/
function printTitle(title) {
return `${title ? `${title}\n` : ''}`;
}
/** @type {Ora} */
let spinner;
class LogService {
/**
* @param {string} text
* @param {string} [title]
*/
static debug(text, title) {
if (!this.debugEnabled) return;
log(chalk.bgCyanBright.black.bold(` debug${printTitle(title)}`), text);
// @ts-ignore
this._logHistory.push(`- debug -${printTitle(title)} ${text}`);
}
/**
* @param {string} text
* @param {string} [title]
*/
static warn(text, title) {
log(chalk.bgYellowBright.black.bold(`warning${printTitle(title)}`), text);
// @ts-ignore
this._logHistory.push(`- warning -${printTitle(title)} ${text}`);
}
/**
* @param {string} text
* @param {string} [title]
*/
static error(text, title) {
log(chalk.bgRedBright.black.bold(` error${printTitle(title)}`), text);
// @ts-ignore
this._logHistory.push(`- error -${printTitle(title)} ${text}`);
}
/**
* @param {string} text
* @param {string} [title]
*/
static success(text, title) {
log(chalk.bgGreen.black.bold(`success${printTitle(title)}`), text);
// @ts-ignore
this._logHistory.push(`- success -${printTitle(title)} ${text}`);
}
/**
* @param {string} text
* @param {string} [title]
*/
static info(text, title) {
log(chalk.bgBlue.black.bold(` info${printTitle(title)}`), text);
// @ts-ignore
this._logHistory.push(`- info -${printTitle(title)} ${text}`);
}
/**
* @param {string} text
*/
static spinnerStart(text) {
spinner = ora(text).start();
}
/**
* @param {string} text
*/
static spinnerText(text) {
if (!spinner) {
this.spinnerStart(text);
@ -56,9 +97,13 @@ class LogService {
return spinner;
}
static pad(str, minChars = 20) {
let result = str;
const padding = minChars - str.length;
/**
* @param {string} text
* @param {number} minChars
*/
static pad(text, minChars = 20) {
let result = text;
const padding = minChars - text.length;
if (padding > 0) {
result += ' '.repeat(padding);
}
@ -68,15 +113,18 @@ class LogService {
static writeLogFile() {
const filePath = pathLib.join(process.cwd(), 'providence.log');
let file = `[log ${new Date()}]\n`;
// @ts-ignore
this._logHistory.forEach(l => {
file += `${l}\n`;
});
file += `[/log ${new Date()}]\n\n`;
fs.writeFileSync(filePath, file, { flag: 'a' });
// @ts-ignore
this._logHistory = [];
}
}
LogService.debugEnabled = false;
/** @type {string[]} */
LogService._logHistory = [];
module.exports = { LogService };

View file

@ -1,35 +1,55 @@
// @ts-ignore-next-line
require('../types/index.js');
const deepmerge = require('deepmerge');
const child_process = require('child_process'); // eslint-disable-line camelcase
const { AstService } = require('./AstService.js');
const { LogService } = require('./LogService.js');
const { getFilePathRelativeFromRoot } = require('../utils/get-file-path-relative-from-root.js');
/**
* @typedef {import('../types/analyzers').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/analyzers').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
* @typedef {import('../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../types/core').QueryConfig} QueryConfig
* @typedef {import('../types/core').QueryResult} QueryResult
* @typedef {import('../types/core').FeatureQueryConfig} FeatureQueryConfig
* @typedef {import('../types/core').SearchQueryConfig} SearchQueryConfig
* @typedef {import('../types/core').AnalyzerQueryConfig} AnalyzerQueryConfig
* @typedef {import('../types/core').Feature} Feature
* @typedef {import('../types/core').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../types/core').Analyzer} Analyzer
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../types/core').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../types/core').GatherFilesConfig} GatherFilesConfig
* @typedef {import('../types/core').AnalyzerQueryResult} AnalyzerQueryResult
* @typedef {import('../types/core').ProjectInputData} ProjectInputData
*/
const astProjectsDataCache = new Map();
class QueryService {
/**
* @param {string} regexString string for 'free' regex searches.
* @returns {QueryConfig}
* @returns {SearchQueryConfig}
*/
static getQueryConfigFromRegexSearchString(regexString) {
return { type: 'search', regexString };
}
/**
* @desc Util function that can be used to parse cli input and feed the result object to a new
* Util function that can be used to parse cli input and feed the result object to a new
* instance of QueryResult
* @example
* const queryConfig = QueryService.getQueryConfigFromFeatureString(tg-icon[size=xs])
* const myQueryResult = QueryService.grepSearch(inputData, queryConfig)
* @param {string} queryString - string like tg-icon[size=xs]
* @returns {QueryConfig}
* @returns {FeatureQueryConfig}
*/
static getQueryConfigFromFeatureString(queryString) {
/**
* @param {string} candidate
* @returns {[string, boolean]}
*/
function parseContains(candidate) {
const hasAsterisk = candidate ? candidate.endsWith('*') : null;
const hasAsterisk = candidate ? candidate.endsWith('*') : false;
const filtered = hasAsterisk ? candidate.slice(0, -1) : candidate;
return [filtered, hasAsterisk];
}
@ -67,16 +87,17 @@ class QueryService {
};
} else {
// Just look for tag name
featureObj = { tag, usesTagPartialMatch };
featureObj = /** @type {Feature} */ ({ tag, usesTagPartialMatch });
}
return { type: 'feature', feature: featureObj };
}
/**
* @desc retrieves the default export found in ./program/analyzers/findImport.js
* @param {string|Analyzer} analyzer
* @returns {QueryConfig}
* RSetrieves the default export found in ./program/analyzers/findImport.js
* @param {string|Analyzer} analyzerObjectOrString
* @param {AnalyzerConfig} analyzerConfig
* @returns {AnalyzerQueryConfig}
*/
static getQueryConfigFromAnalyzer(analyzerObjectOrString, analyzerConfig) {
let analyzer;
@ -85,7 +106,7 @@ class QueryService {
// Mainly needed when this method is called via cli
try {
// eslint-disable-next-line import/no-dynamic-require, global-require
analyzer = require(`../analyzers/${analyzerObjectOrString}`);
analyzer = /** @type {Analyzer} */ (require(`../analyzers/${analyzerObjectOrString}`));
} catch (e) {
LogService.error(e);
process.exit(1);
@ -95,8 +116,8 @@ class QueryService {
analyzer = analyzerObjectOrString;
}
return {
type: 'analyzer',
analyzerName: analyzer.name,
type: 'ast-analyzer',
analyzerName: /** @type {AnalyzerName} */ (analyzer.name),
analyzerConfig,
analyzer,
};
@ -169,27 +190,27 @@ class QueryService {
}
/**
* @desc Search via ast (typescript compilation)
* @param {QueryConfig} queryConfig
* Search via ast (typescript compilation)
* @param {AnalyzerQueryConfig} analyzerQueryConfig
* @param {AnalyzerConfig} [customConfig]
* @param {GatherFilesConfig} [customConfig.gatherFilesConfig]
* @returns {QueryResult}
* @returns {Promise<AnalyzerQueryResult>}
*/
static async astSearch(queryConfig, customConfig) {
static async astSearch(analyzerQueryConfig, customConfig) {
LogService.debug('started astSearch method');
if (queryConfig.type !== 'analyzer') {
if (analyzerQueryConfig.type !== 'ast-analyzer') {
LogService.error('Only analyzers supported for ast searches at the moment');
process.exit(1);
}
// @ts-ignore
// eslint-disable-next-line new-cap
const analyzer = new queryConfig.analyzer();
const analyzer = new analyzerQueryConfig.analyzer();
const analyzerResult = await analyzer.execute(customConfig);
if (!analyzerResult) {
return analyzerResult;
}
const { queryOutput, analyzerMeta } = analyzerResult;
const /** @type {QueryResult} */ queryResult = {
const /** @type {AnalyzerQueryResult} */ queryResult = {
meta: {
searchType: 'ast-analyzer',
analyzerMeta,
@ -200,7 +221,7 @@ class QueryService {
}
/**
* @param {ProjectData[]} projectsData
* @param {ProjectInputData[]} projectsData
* @param {'babel'|'typescript'|'es-module-lexer'} requiredAst
*/
static async addAstToProjectsData(projectsData, requiredAst) {
@ -242,13 +263,8 @@ class QueryService {
}
/**
* @desc Performs a grep on given path for a certain tag name and feature
* @param {string} searchPath - the project path to search in
* Performs a grep on given path for a certain tag name and feature
* @param {Feature} feature
* @param {object} [customConfig]
* @param {boolean} [customConfig.count] - enable wordcount in grep
* @param {GatherFilesConfig} [customConfig.gatherFilesConfig] - extensions, excludes
* @param {boolean} [customConfig.hasDebugEnabled]
*/
static _getFeatureRegex(feature) {
const { name, value, tag } = feature;
@ -287,6 +303,13 @@ class QueryService {
return regex;
}
/**
*
* @param {PathFromSystemRoot} searchPath
* @param {string} regex
* @param {{ count:number; gatherFilesConfig:GatherFilesConfig; hasDebugEnabled:boolean }} customConfig
* @returns
*/
static _performGrep(searchPath, regex, customConfig) {
const cfg = deepmerge(
{

View file

@ -1,21 +1,28 @@
// @ts-ignore-next-line
require('../types/index.js');
const fs = require('fs');
const pathLib = require('path');
const getHash = require('../utils/get-hash.js');
/**
* @desc Should be used to write results to and read results from the file system.
* @typedef {import('../types/core').Project} Project
* @typedef {import('../types/core').ProjectName} ProjectName
* @typedef {import('../types/core').AnalyzerQueryResult} AnalyzerQueryResult
* @typedef {import('../types/core').AnalyzerConfig} AnalyzerConfig
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../types/core').QueryResult} QueryResult
* @typedef {import('../types/core').PathFromSystemRoot} PathFromSystemRoot
*/
/**
* Should be used to write results to and read results from the file system.
* Creates a unique identifier based on searchP, refP (optional) and an already created
* @param {object} searchP search target project meta
* @param {object} cfg configuration used for analyzer
* @param {object} [refP] reference project meta
* @param {Project} searchP search target project meta
* @param {AnalyzerConfig} cfg configuration used for analyzer
* @param {Project} [refP] reference project meta
* @returns {string} identifier
*/
function createResultIdentifier(searchP, cfg, refP) {
// why encodeURIComponent: filters out slashes for path names for stuff like @lion/button
const format = p =>
const format = (/** @type {Project} */ p) =>
`${encodeURIComponent(p.name)}_${p.version || (p.commitHash && p.commitHash.slice(0, 5))}`;
const cfgHash = getHash(cfg);
return `${format(searchP)}${refP ? `_+_${format(refP)}` : ''}__${cfgHash}`;
@ -23,7 +30,6 @@ function createResultIdentifier(searchP, cfg, refP) {
class ReportService {
/**
* @desc
* Prints queryResult report to console
* @param {QueryResult} queryResult
*/
@ -38,15 +44,14 @@ class ReportService {
}
/**
* @desc
* Prints queryResult report as JSON to outputPath
* @param {QueryResult} queryResult
* @param {AnalyzerQueryResult} queryResult
* @param {string} [identifier]
* @param {string} [outputPath]
*/
static writeToJson(
queryResult,
identifier = new Date().getTime() / 1000,
identifier = (new Date().getTime() / 1000).toString(),
outputPath = this.outputPath,
) {
const output = JSON.stringify(queryResult, null, 2);
@ -58,18 +63,29 @@ class ReportService {
fs.writeFileSync(filePath, output, { flag: 'w' });
}
static set outputPath(p) {
this.__outputPath = p;
}
/**
* @type {string}
*/
static get outputPath() {
return this.__outputPath || pathLib.join(process.cwd(), '/providence-output');
}
static set outputPath(p) {
this.__outputPath = p;
}
/**
* @param {{ targetProject: Project; referenceProject: Project; analyzerConfig: AnalyzerConfig }} options
* @returns {string}
*/
static createIdentifier({ targetProject, referenceProject, analyzerConfig }) {
return createResultIdentifier(targetProject, analyzerConfig, referenceProject);
}
/**
* @param {{analyzerName: AnalyzerName; identifier: string}} options
* @returns {QueryResult}
*/
static getCachedResult({ analyzerName, identifier }) {
let cachedResult;
try {
@ -81,10 +97,21 @@ class ReportService {
return cachedResult;
}
/**
* @param {string} name
* @param {string} identifier
* @returns {PathFromSystemRoot}
*/
static _getResultFileNameAndPath(name, identifier) {
return pathLib.join(this.outputPath, `${name || 'query'}_-_${identifier}.json`);
return /** @type {PathFromSystemRoot} */ (
pathLib.join(this.outputPath, `${name || 'query'}_-_${identifier}.json`)
);
}
/**
* @param {ProjectName} depProj
* @param {Project} rootProjectMeta
*/
static writeEntryToSearchTargetDepsFile(depProj, rootProjectMeta) {
const rootProj = `${rootProjectMeta.name}#${rootProjectMeta.version}`;
const filePath = pathLib.join(this.outputPath, 'search-target-deps-file.json');

View file

@ -0,0 +1,63 @@
import {
PathFromSystemRoot,
IdentifierName,
RootFile,
AnalyzerQueryResult,
FindAnalyzerOutputFile,
} from '../core';
export interface FindClassesAnalyzerResult extends AnalyzerQueryResult {
queryOutput: FindClassesAnalyzerOutputFile[];
}
export interface FindClassesAnalyzerOutputFile extends FindAnalyzerOutputFile {
/** result of AST traversal for file in project */
result: FindClassesAnalyzerEntry[];
}
export interface FindClassesAnalyzerEntry {
/** the name of the class */
name: IdentifierName;
/** whether the class is a mixin function */
isMixin: boolean;
/** super classes and mixins */
superClasses: SuperClass[];
members: ClassMember;
}
interface ClassMember {
props: ClassProperty;
methods: ClassMethod;
}
type AccessType = 'public' | 'protected' | 'private';
interface ClassProperty {
/** class property name */
name: IdentifierName;
/** 'public', 'protected' or 'private' */
accessType: AccessType;
/** can be 'get', 'set' or both */
kind: ('get' | 'set')[];
/** whether property is static */
static: boolean;
}
interface ClassMethod {
/** class method name */
name: IdentifierName;
accessType: AccessType;
}
export interface SuperClass {
/** the name of the super class */
name: IdentifierName;
/** whether the superClass is a mixin function */
isMixin: boolean;
rootFile: RootFile;
}
export interface FindClassesConfig {
/** search target paths */
targetProjectPath: PathFromSystemRoot;
}

View file

@ -0,0 +1,33 @@
import {
PathRelativeFromProjectRoot,
IdentifierName,
RootFile,
AnalyzerQueryResult,
FindAnalyzerOutputFile,
} from '../core';
export interface FindCustomelementsAnalyzerResult extends AnalyzerQueryResult {
queryOutput: FindCustomelementsAnalyzerOutputFile[];
}
export interface FindCustomelementsAnalyzerOutputFile extends FindAnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: PathRelativeFromProjectRoot;
/** result of AST traversal for file in project */
result: FindCustomelementsAnalyzerEntry[];
}
export interface FindCustomelementsAnalyzerEntry {
/**
* Tag name found in CE definition:
* `customElements.define('my-name', MyConstructor)` => 'my-name'
*/
tagName: string;
/**
* Identifier found in CE definition:
* `customElements.define('my-name', MyConstructor)` => MyConstructor
*/
constructorIdentifier: IdentifierName;
/** Rootfile traced for constuctorIdentifier found in CE definition */
rootFile: RootFile;
}

View file

@ -0,0 +1,67 @@
import {
SpecifierName,
SpecifierSource,
PathRelativeFromProjectRoot,
RootFileMapEntry,
RootFile,
AnalyzerQueryResult,
FindAnalyzerOutputFile,
} from '../core';
export interface FindExportsAnalyzerResult extends AnalyzerQueryResult {
queryOutput: FindExportsAnalyzerOutputFile[];
}
export interface FindExportsAnalyzerOutputFile extends FindAnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: PathRelativeFromProjectRoot;
/** result of AST traversal for file in project */
result: FindExportsAnalyzerEntry[];
}
export interface FindExportsAnalyzerEntry {
/**
* The specifiers found in an export statement.
*
* For example:
* - file `export class X {}` gives `['X']`
* - file `export default const y = 0` gives `['[default]']`
* - file `export { y, z } from 'project'` gives `['y', 'z']`
*/
exportSpecifiers: SpecifierName[];
/**
* The original "source" string belonging to specifier.
* For example:
* - file `export { x } from './my/file';` gives `"./my/file"`
* - file `export { x } from 'project';` gives `"project"`
*/
source: SpecifierSource;
/**
* The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions).
* For example:
* - file `export { x } from './my/file';` gives `"./my/file.js"`
* - file `export { x } from 'project';` gives `"project"` (only files in current project are resolved)
* - file `export { x } from '../';` gives `"../index.js"`
*/
normalizedSource: SpecifierSource;
/** map of tracked down Identifiers */
rootFileMap: RootFileMapEntry[];
}
/**
* Iterable version of `FindExportsAnalyzerEntry`.
* Makes it easier to do comparisons inside MatchAnalyzers
*/
export interface IterableFindExportsAnalyzerEntry {
file: PathRelativeFromProjectRoot;
specifier: SpecifierName;
/**
* The local name of an export. Example:
* 'a' in case of `export {a as b} from 'c';`
*/
localSpecifier: SpecifierName;
source: SpecifierSource | null;
rootFile: RootFile;
meta?: object;
}

View file

@ -0,0 +1,58 @@
import {
SpecifierName,
SpecifierSource,
PathRelativeFromProjectRoot,
AnalyzerQueryResult,
FindAnalyzerOutputFile,
} from '../core';
export interface FindImportsAnalyzerResult extends AnalyzerQueryResult {
queryOutput: FindImportsAnalyzerOutputFile[];
}
export interface FindImportsAnalyzerOutputFile extends FindAnalyzerOutputFile {
/** result of AST traversal for file in project */
result: FindImportsAnalyzerEntry[];
}
export interface FindImportsAnalyzerEntry {
/**
* The specifiers found in an import statement.
*
* For example:
* - file `import { X } from 'project'` gives `['X']`
* - file `import X from 'project'` gives `['[default]']`
* - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']`
*/
importSpecifiers: SpecifierName[];
/**
* The original "source" string belonging to specifier.
* For example:
* - file `import { x } from './my/file';` gives `"./my/file"`
* - file `import { x } from 'project';` gives `"project"`
*/
source: SpecifierSource;
/**
* The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions).
* For example:
* - file `import { x } from './my/file';` gives `"./my/file.js"`
* - file `import { x } from 'project';` gives `"project"` (only files in current project are resolved)
* - file `import { x } from '../';` gives `"../index.js"`
*/
normalizedSource: SpecifierSource;
}
/**
* Iterable version of `FindExportsAnalyzerEntry`.
* Makes it easier to do comparisons inside MatchAnalyzers
*/
export interface IterableFindImportsAnalyzerEntry {
file: PathRelativeFromProjectRoot;
specifier: SpecifierName;
source: SpecifierSource;
/**
* Resolved `SpecifierSource` o relative file path
*/
normalizedSource: SpecifierSource;
}

View file

@ -0,0 +1,6 @@
export * from './find-classes';
export * from './find-customelements';
export * from './find-exports';
export * from './find-imports';
export * from './match-imports';
export * from './match-subclasses';

View file

@ -0,0 +1,27 @@
import { ImportOrExportId, PathRelativeFromProjectRoot, ProjectName } from '../core/core';
import { AnalyzerQueryResult, MatchedExportSpecifier, MatchAnalyzerConfig } from '../core/Analyzer';
export interface MatchImportsAnalyzerResult extends AnalyzerQueryResult {
queryOutput: MatchImportsAnalyzerOutputEntry[];
}
export interface MatchImportsAnalyzerOutputEntry {
exportSpecifier: MatchedExportSpecifier;
matchesPerProject: MatchImportsAnalyzerOutputEntryMatch[];
}
export interface MatchImportsAnalyzerOutputEntryMatch {
/** The target project that extends the class exported by reference project */
project: ProjectName;
/** Array of meta objects for matching files */
files: PathRelativeFromProjectRoot[];
}
export type ConciseMatchImportsAnalyzerResult = ConciseMatchImportsAnalyzerResultEntry[];
export interface ConciseMatchImportsAnalyzerResultEntry {
exportSpecifier: { id: ImportOrExportId; meta?: object };
importProjectFiles: PathRelativeFromProjectRoot[];
}
export interface MatchImportsConfig extends MatchAnalyzerConfig {}

View file

@ -0,0 +1,38 @@
import {
SpecifierName,
ProjectName,
PathRelativeFromProjectRoot,
AnalyzerQueryResult,
MatchedExportSpecifier,
} from '../core';
export interface MatchSubclassesAnalyzerResult extends AnalyzerQueryResult {
queryOutput: MatchSubclassesAnalyzerOutputEntry[];
}
export interface MatchSubclassesAnalyzerOutputEntry {
exportSpecifier: MatchedExportSpecifier;
matchesPerProject: MatchSubclassesAnalyzerOutputEntryMatch[];
}
export interface MatchSubclassesAnalyzerOutputEntryMatch {
/** The target project that extends the class exported by reference project */
project: ProjectName;
/** Array of meta objects for matching files */
files: MatchSubclassesAnalyzerOutputEntryMatchFile[];
}
export interface MatchSubclassesAnalyzerOutputEntryMatchFile {
/**
* The local filepath that contains the matched class inside the target project
* like `./src/ExtendedClass.js`
*/
file: PathRelativeFromProjectRoot;
/**
* The local Identifier inside matched file that is exported
* @example
* - `ExtendedClass` for `export ExtendedClass extends RefClass {};`
* - `[default]` for `export default ExtendedClass extends RefClass {};`
*/
identifier: SpecifierName;
}

View file

@ -0,0 +1,78 @@
import {
PathRelativeFromProjectRoot,
PathFromSystemRoot,
QueryType,
QueryResult,
RequiredAst,
ImportOrExportId,
Project,
GatherFilesConfig,
SpecifierName,
} from './index';
/**
* Name of the analyzer, like 'find-imports' or 'match-sublcasses'
*/
export type AnalyzerName = `${'find' | 'match'}-${string}`;
// TODO: make sure that data structures of JSON output (generated in ReportService)
// and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize)
// so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema
export interface Meta {
searchType: QueryType;
/** analyzer meta object */
analyzerMeta: AnalyzerMeta;
}
export interface AnalyzerMeta {
name: AnalyzerName;
requiredAst: RequiredAst;
/** a unique hash based on target, reference and configuration */
identifier: ImportOrExportId;
/** target project meta object */
targetProject: Project;
/** reference project meta object */
referenceProject?: Project;
/** the configuration used for this particular analyzer run */
configuration: object;
}
export interface AnalyzerQueryResult extends QueryResult {
/** meta info object */
meta: Meta;
/** array of AST traversal output, per project file */
queryOutput: any[];
}
export interface FindAnalyzerQueryResult extends AnalyzerQueryResult {
queryOutput: FindAnalyzerOutputFile[];
}
export interface FindAnalyzerOutputFile {
/** path relative from project root for which a result is generated based on AST traversal */
file: PathRelativeFromProjectRoot;
/** result of AST traversal for file in project */
result: any[];
}
export interface AnalyzerConfig {
/** search target project path */
targetProjectPath: PathFromSystemRoot;
gatherFilesConfig: GatherFilesConfig;
}
export interface MatchAnalyzerConfig extends AnalyzerConfig {
/** reference project path, used to match reference against target */
referenceProjectPath: PathFromSystemRoot;
targetProjectResult: AnalyzerQueryResult;
referenceProjectResult: AnalyzerQueryResult;
}
export interface MatchedExportSpecifier extends AnalyzerQueryResult {
name: SpecifierName;
/** Project name as found in package.json */
project: string;
/** Path relative from project root, for instance `./index.js` */
filePath: PathRelativeFromProjectRoot;
id: ImportOrExportId;
}

View file

@ -0,0 +1,48 @@
import { AnalyzerName, Feature, AnalyzerConfig, PathRelativeFromProjectRoot } from './index';
import { Analyzer } from '../../analyzers/helpers/Analyzer';
export { Analyzer } from '../../analyzers/helpers/Analyzer';
/**
* Type of the query. Currently only "ast-analyzer" supported
*/
export type QueryType = 'ast-analyzer' | 'search' | 'feature';
/** an object containing keys name, value, term, tag */
export interface QueryConfig {
/**
* The type of the tag we are searching for.
* A certain type has an additional property with more detailed information about the type
*/
type: QueryType;
}
export interface AnalyzerQueryConfig extends QueryConfig {
/** query details for a feature search */
analyzer: Analyzer;
analyzerName: AnalyzerName;
analyzerConfig: AnalyzerConfig;
}
export interface FeatureQueryConfig extends QueryConfig {
/** query details for a feature search */
feature: Feature;
}
export interface SearchQueryConfig extends QueryConfig {
/** if type is 'search', a regexString should be provided */
regexString: string;
}
export interface QueryOutputEntry {
result: any;
file: PathRelativeFromProjectRoot;
}
export type QueryOutput = QueryOutputEntry[];
export interface QueryResult {
queryOutput: QueryOutput;
meta: {
searchType: QueryType;
};
}

View file

@ -0,0 +1,151 @@
/**
* The name of a variable in a local context. Examples:
* - 'b': (`import {a as b } from 'c';`)
* - 'MyClass': (`class MyClass {}`)
*/
export type IdentifierName = string;
/**
* The string representation of an export.
* Examples:
* - 'x': (`export { x } from 'y';` or `import { x } from 'y';`)
* - '[default]': (`export x from 'y';` or `import x from 'y';`)
* - '[*]': (`export * from 'y';` or `import * as allFromY from 'y';`)
*/
export type SpecifierName = '[default]' | '[*]' | IdentifierName;
/**
* Source of import or export declaration. Examples:
* - 'my/external/project'
* - './my/local/file.js'
*/
export type SpecifierSource = string;
/**
* The resolved filePath relative from project root.
* Examples:
* - "./index.js"
* - "./src/helpers/foo.js"
*/
export type PathRelative = `${'./' | '../'}${string}`;
/**
* The resolved filePath relative from project root.
* Examples:
* - "./index.js"
* - "./src/helpers/foo.js"
*/
export type PathRelativeFromProjectRoot = `./${string}`;
/**
* Posix compatible path from system root.
* Example:
* - "/my/projects/project-x"
*/
export type PathFromSystemRoot = `/${string}`;
export type RootFile = {
/** the file path containing declaration, for instance './target-src/direct-imports.js'. Can also contain keyword '[current]' */
file: string;
/** the specifier/identifier that was exported in root file, for instance 'MyClass' */
specifier: SpecifierName;
};
/**
* Required ast for the analysis. Currently, only Babel is supported
*/
export type RequiredAst = 'babel';
/**
* Name entry found in package.json
*/
export type ProjectName = string;
export type ImportOrExportId = `${SpecifierName}::${PathRelativeFromProjectRoot}::${ProjectName}`;
export interface RootFileMapEntry {
/** This is the local name in the file we track from */
currentFileSpecifier: SpecifierName;
/**
* The file that contains the original declaration of a certain Identifier/Specifier.
* Contains file(filePath) and specifier keys
*/
rootFile: RootFile;
}
export interface Project {
/** "name" found in package.json and under which the package is registered in npm */
name: ProjectName;
/** "version" found in package.json */
version: string;
/** "main" File found in package.json */
mainEntry: PathRelativeFromProjectRoot;
/** if a git repo is analyzed, stores commit hash, [not-a-git-repo] if not */
commitHash: '[not-a-git-repo]' | string;
path: PathFromSystemRoot;
}
export interface Feature {
/** the name of the feature. For instance 'size' */
name?: string;
/** the value of the feature. For instance 'xl' */
value?: string;
/** the name of the object this feature belongs to. */
memberOf?: string;
/**
* the HTML element it belongs to. Will be used in html
* queries. This option will take precedence over 'memberOf' when configured
*/
tag?: string;
/**
* useful for HTML queries explicitly looking for attribute
* name instead of property name. When false(default), query searches for properties
*/
isAttribute?: boolean;
/** when the attribute value is not an exact match */
usesValueContains?: boolean;
/**
* when looking for a partial match:
* div[class*=foo*] -> <div class="baz foo-bar">*/
usesValuePartialMatch?: boolean;
/**
* when looking for an exact match inside a space
* separated list within an attr: div[class*=foo] -> <div class="baz foo bar">
*/
usesTagPartialMatch?: boolean;
}
export interface GatherFilesConfig {
/** file extension like ['.js', '.html'] */
extensions: `.${string}`[];
filter?: AnyMatchString[];
omitDefaultAllowlist?: boolean;
depth?: number;
allowlist: string[];
allowlistMode?: 'npm' | 'git' | 'all';
}
export interface ProjectInputData {
project: Project;
/**
* Array of paths that are found within 'project' that
* comply to the rules as configured in 'gatherFilesConfig'
*/
entries: PathFromSystemRoot[];
}
export interface ProjectInputDataWithMeta {
project: Project;
entries: { file: PathRelativeFromProjectRoot; context: { code: string } }[];
}
/**
* See: https://www.npmjs.com/package/anymatch
* Allows negations as well. See: https://www.npmjs.com/package/is-negated-glob
* Examples:
* - `'scripts/**\/*.js'
* - '!scripts / vendor/**'
* - 'scripts/vendor/react.js'
*/
export type AnyMatchString = string;

View file

@ -0,0 +1,3 @@
export * from './core';
export * from './Analyzer';
export * from './QueryService';

View file

@ -1,65 +0,0 @@
/**
* @typedef {Object} Feature
* @property {string} [name] the name of the feature. For instance 'size'
* @property {string} [value] the value of the feature. For instance 'xl'
* @property {string} [memberOf] the name of the object this feature belongs to.
*
* @property {string} [tag] the HTML element it belongs to. Will be used in html
* queries. This option will take precedence over 'memberOf' when configured
* @property {boolean} [isAttribute] useful for HTML queries explicitly looking for attribute
* name instead of property name. When false(default), query searches for properties
* @property {boolean} [usesValueContains] when the attribute value is not an exact match
* @property {boolean} [usesValuePartialMatch] when looking for a partial match:
* div[class*=foo*] -> <div class="baz foo-bar">
* @property {boolean} [usesTagPartialMatch] when looking for an exact match inside a space
* separated list within an attr: div[class*=foo] -> <div class="baz foo bar">
*/
/**
* @typedef {Object} QueryResult result of a query. For all projects and files, gives the
* result of the query.
* @property {Object} QueryResult.meta
* @property {'ast'|'grep'} QueryResult.meta.searchType
* @property {QueryConfig} QueryResult.meta.query
* @property {Object[]} QueryResult.results
* @property {string} QueryResult.queryOutput[].project project name as determined by InputDataService (based on folder name)
* @property {number} QueryResult.queryOutput[].count
* @property {Object[]} [QueryResult.queryOutput[].files]
* @property {string} QueryResult.queryOutput[].files[].file
* @property {number} QueryResult.queryOutput[].files[].line
* @property {string} QueryResult.queryOutput[].files[].match
*/
/**
* @typedef {object} QueryConfig an object containing keys name, value, term, tag
* @property {string} QueryConfig.type the type of the tag we are searching for.
* A certain type has an additional property with more detailed information about the type
* @property {Feature} feature query details for a feature search
*/
/**
* @typedef {Object} InputDataProject - all files found that are queryable
* @property {string} InputDataProject.project - the project name
* @property {string} InputDataProject.path - the path to the project
* @property {string[]} InputDataProject.entries - array of paths that are found within 'project' that
* comply to the rules as configured in 'gatherFilesConfig'
*/
/**
* @typedef {InputDataProject[]} InputData - all files found that are queryable
*/
/**
* @typedef {Array|String|RegExp|Function} AnyMatchString see: https://www.npmjs.com/package/anymatch
* Allows negations as well. See: https://www.npmjs.com/package/is-negated-glob
* @example
* `'scripts/**\/*.js'
* '!scripts / vendor/**'
* 'scripts/vendor/react.js'
*/
/**
* @typedef {Object} GatherFilesConfig
* @property {string[]} [extensions] file extension like ['.js', '.html']
* @property {AnyMatchString[]} [filter] file patterns filtered out. See: https://www.npmjs.com/package/anymatch
*/

View file

@ -1,5 +1,5 @@
/**
* @desc Readable way to do an async forEach
* Readable way to do an async forEach
* Since predictability matters, all array items will be handled in a queue,
* one after another
* @param {any[]} array
@ -12,7 +12,7 @@ async function aForEach(array, callback) {
}
}
/**
* @desc Readable way to do an async forEach
* Readable way to do an async forEach
* If predictability does not matter, this method will traverse array items concurrently,
* leading to a better performance
* @param {any[]} array
@ -22,11 +22,11 @@ async function aForEachNonSequential(array, callback) {
return Promise.all(array.map(callback));
}
/**
* @desc Readable way to do an async map
* Readable way to do an async map
* Since predictability is crucial for a map, all array items will be handled in a queue,
* one after anotoher
* @param {any[]} array
* @param {function} callback
* @param {Array<any>} array
* @param {(param:any, i:number) => any} callback
*/
async function aMap(array, callback) {
const mappedResults = [];

View file

@ -1,12 +1,18 @@
/**
* @desc relative path of analyzed file, realtive to project root of analyzed project
* @typedef {import('../types/core/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../types/core/core').PathFromSystemRoot} PathFromSystemRoot
*/
/**
* Relative path of analyzed file, realtive to project root of analyzed project
* - from: '/my/machine/details/analyzed-project/relevant/file.js'
* - to: './relevant/file.js'
* @param {string} absolutePath
* @param {string} projectRoot
* @param {PathFromSystemRoot} absolutePath
* @param {PathFromSystemRoot} projectRoot
* @returns {PathRelativeFromProjectRoot}
*/
function getFilePathRelativeFromRoot(absolutePath, projectRoot) {
return absolutePath.replace(projectRoot, '.');
return /** @type {PathRelativeFromProjectRoot} */ (absolutePath.replace(projectRoot, '.'));
}
module.exports = { getFilePathRelativeFromRoot };

View file

@ -2,10 +2,9 @@ const { InputDataService } = require('../services/InputDataService.js');
/**
* @param {function} func
* @param {{}} externalStorage
* @param {object} [storage]
*/
function memoize(func, externalStorage) {
const storage = externalStorage || {};
function memoize(func, storage = {}) {
// eslint-disable-next-line func-names
return function () {
// eslint-disable-next-line prefer-rest-params
@ -19,6 +18,7 @@ function memoize(func, externalStorage) {
// @ts-ignore
const outcome = func.apply(this, args);
// @ts-ignore
// eslint-disable-next-line no-param-reassign
storage[args] = outcome;
return outcome;
};
@ -26,10 +26,9 @@ function memoize(func, externalStorage) {
/**
* @param {function} func
* @param {{}} externalStorage
* @param {object} [storage]
*/
function memoizeAsync(func, externalStorage) {
const storage = externalStorage || {};
function memoizeAsync(func, storage = {}) {
// eslint-disable-next-line func-names
return async function () {
// eslint-disable-next-line prefer-rest-params
@ -43,6 +42,7 @@ function memoizeAsync(func, externalStorage) {
// @ts-ignore
const outcome = await func.apply(this, args);
// @ts-ignore
// eslint-disable-next-line no-param-reassign
storage[args] = outcome;
return outcome;
};

View file

@ -3,6 +3,12 @@
* https://github.com/open-wc/open-wc/blob/master/packages/es-dev-server/src/utils/resolve-module-imports.js
*/
/**
* @typedef {import('../types/core/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
* @typedef {import('../types/core/core').PathFromSystemRoot} PathFromSystemRoot
* @typedef {import('../types/core/core').SpecifierSource} SpecifierSource
*/
const pathLib = require('path');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { LogService } = require('../services/LogService.js');
@ -13,6 +19,9 @@ const fakePluginContext = {
rollupVersion: '^2.42.0',
},
resolve: () => {},
/**
* @param {string[]} msg
*/
warn(...msg) {
LogService.warn('[resolve-import-path]: ', ...msg);
},
@ -22,9 +31,10 @@ const fakePluginContext = {
* Based on importee (in a statement "import {x} from '@lion/core'", "@lion/core" is an
* importee), which can be a bare module specifier, a filename without extension, or a folder
* name without an extension.
* @param {string} importee source like '@lion/core'
* @param {string} importer importing file, like '/my/project/importing-file.js'
* @returns {string} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
* @param {SpecifierSource} importee source like '@lion/core' or '../helpers/index.js'
* @param {PathFromSystemRoot} importer importing file, like '/my/project/importing-file.js'
* @param {{customResolveOptions?: {preserveSymlinks:boolean}}} [opts] nodeResolve options
* @returns {Promise<PathFromSystemRoot|null>} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
*/
async function resolveImportPath(importee, importer, opts = {}) {
const rollupResolve = nodeResolve({
@ -37,14 +47,18 @@ async function resolveImportPath(importee, importer, opts = {}) {
const preserveSymlinks =
(opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
// @ts-ignore
rollupResolve.buildStart.call(fakePluginContext, { preserveSymlinks });
// @ts-ignore
const result = await rollupResolve.resolveId.call(fakePluginContext, importee, importer, {});
// @ts-ignore
if (!result || !result.id) {
// throw new Error(`importee ${importee} not found in filesystem.`);
LogService.warn(`importee ${importee} not found in filesystem for importer '${importer}'.`);
return null;
}
// @ts-ignore
return result.id;
}

View file

@ -1,12 +1,16 @@
/**
* @param {string} pathStr C:\Example\path/like/this
* returns {string} /Example/path/like/this
* @typedef {import('../types/core/core').PathFromSystemRoot} PathFromSystemRoot
*/
/**
* @param {PathFromSystemRoot|string} pathStr C:\Example\path/like/this
* @returns {PathFromSystemRoot} /Example/path/like/this
*/
function toPosixPath(pathStr) {
if (process.platform === 'win32') {
return pathStr.replace(/^.:/, '').replace(/\\/g, '/');
return /** @type {PathFromSystemRoot} */ (pathStr.replace(/^.:/, '').replace(/\\/g, '/'));
}
return pathStr;
return /** @type {PathFromSystemRoot} */ (pathStr);
}
module.exports = { toPosixPath };

View file

@ -6,9 +6,9 @@ const /** @type {PostProcessorOptions} */ options = {
/**
*
* @param {AnalyzerResult} analyzerResult
* @param {AnalyzerQueryResult} analyzerResult
* @param {FindImportsConfig} customConfig
* @returns {AnalyzerResult}
* @returns {AnalyzerQueryResult}
*/
function myPostProcessor(analyzerResult, customConfig) {
const cfg = {
@ -31,7 +31,7 @@ function myPostProcessor(analyzerResult, customConfig) {
transformedResult = options.optionA(transformedResult);
}
return /** @type {AnalyzerResult} */ transformedResult;
return /** @type {AnalyzerQueryResult} */ transformedResult;
}
module.exports = {

View file

@ -80,7 +80,7 @@ describe('Analyzer', () => {
describe('Traverse phase', () => {});
describe('Finalize phase', () => {
it('returns an AnalyzerResult', async () => {
it('returns an AnalyzerQueryResult', async () => {
const queryResult = queryResults[0];
const { queryOutput, meta } = queryResult;

View file

@ -22,7 +22,6 @@ const _providenceCfg = {
describe('Analyzer "find-imports"', () => {
const queryResults = [];
const cacheDisabledInitialValue = QueryService.cacheDisabled;
before(() => {

View file

@ -185,7 +185,7 @@ const expectedExportIds = [
...expectedExportIdsNamespaced,
];
// 3. The AnalyzerResult generated by "match-imports"
// 3. The AnalyzerQueryResult generated by "match-imports"
// eslint-disable-next-line no-unused-vars
const expectedMatchesOutput = [
{

View file

@ -105,7 +105,7 @@ const expectedExportIdsDirect = [
// eslint-disable-next-line no-unused-vars
const expectedExportIds = [...expectedExportIdsIndirect, ...expectedExportIdsDirect];
// 3. The AnalyzerResult generated by "match-subclasses"
// 3. The AnalyzerQueryResult generated by "match-subclasses"
// eslint-disable-next-line no-unused-vars
const expectedMatchesOutput = [
{

View file

@ -27,7 +27,7 @@ describe('InputDataService', () => {
it('allows to set targetProjectPaths', async () => {
const newPaths = ['/my/path', '/my/other/path'];
InputDataService.targetProjectPaths = newPaths;
expect(InputDataService.getTargetProjectPaths()).to.equal(newPaths);
expect(InputDataService.targetProjectPaths).to.equal(newPaths);
});
});
@ -61,7 +61,7 @@ describe('InputDataService', () => {
);
});
it('"getTargetProjectPaths"', async () => {});
it('"targetProjectPaths"', async () => {});
it('"getReferenceProjectPaths"', async () => {});