lion/packages-node/providence-analytics/test-node/program/analyzers/helpers/track-down-identifier.test.js

429 lines
13 KiB
JavaScript

import { expect } from 'chai';
import { it } from 'mocha';
import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.js';
import { mockProject } from '../../../../test-helpers/mock-project-helpers.js';
import { swcTraverse } from '../../../../src/program/utils/swc-traverse.js';
import { AstService } from '../../../../src/program/core/AstService.js';
import {
trackDownIdentifier,
trackDownIdentifierFromScope,
} from '../../../../src/program/utils/track-down-identifier.js';
/**
* @typedef {import('@babel/traverse').NodePath} NodePath
*/
setupAnalyzerTest();
describe('trackdownIdentifier', () => {
it(`tracks down identifier to root file (file that declares identifier)`, async () => {
mockProject(
{
'./src/declarationOfMyClass.js': `
export class MyClass extends HTMLElement {}
`,
'./currentFile.js': `
import { MyClass } from './src/declarationOfMyClass';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = './src/declarationOfMyClass';
const identifierName = 'MyClass';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './src/declarationOfMyClass.js',
specifier: 'MyClass',
});
});
it(`tracks down transitive and renamed identifiers`, async () => {
mockProject(
{
'./src/declarationOfMyClass.js': `
export class MyClass extends HTMLElement {}
`,
'./src/renamed.js': `
export { MyClass as MyRenamedClass } from './declarationOfMyClass.js';
`,
'./currentFile.js': `
import { MyRenamedClass } from './src/renamed';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = './src/renamed';
const identifierName = 'MyRenamedClass';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './src/declarationOfMyClass.js',
specifier: 'MyClass',
});
});
it(`tracks down default identifiers`, async () => {
mockProject(
{
'./src/declarationOfMyClass.js': `
export default class MyClass extends HTMLElement {}
`,
'./src/renamed.js': `
import MyClassDefaultReexport from './declarationOfMyClass.js';
export default MyClassDefaultReexport;
`,
'./currentFile.js': `
import MyClassDefaultImport from './src/renamed';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = './src/renamed';
const identifierName = '[default]';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './src/declarationOfMyClass.js',
specifier: '[default]',
});
});
it(`does not track down external sources`, async () => {
mockProject(
{
'./currentFile.js': `
import MyClassDefaultImport from '@external/source';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = '@external/source';
const identifierName = '[default]';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: '@external/source',
specifier: '[default]',
});
});
it(`identifies import map entries as internal sources`, async () => {
mockProject(
{
'./MyClass.js': `export default class {}`,
'./currentFile.js': `
import MyClass from '#internal/source';
`,
'./package.json': JSON.stringify({
name: 'my-project',
imports: { '#internal/source': './MyClass.js' },
}),
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = '#internal/source';
const identifierName = '[default]';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './MyClass.js',
specifier: '[default]',
});
});
it(`self-referencing projects are recognized as internal source`, async () => {
// https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name
mockProject(
{
'./MyClass.js': `export default class {}`,
'./currentFile.js': `
import MyClass from 'my-project/MyClass.js';
`,
'./package.json': JSON.stringify({
name: 'my-project',
exports: { './MyClass.js': './MyClass.js' },
}),
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = 'my-project/MyClass.js';
const identifierName = '[default]';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const projectName = 'my-project';
const rootFile = await trackDownIdentifier(
source,
identifierName,
currentFilePath,
rootPath,
projectName,
);
expect(rootFile).to.deep.equal({
file: './MyClass.js',
specifier: '[default]',
});
});
it(`tracks down locally declared, reexported identifiers (without a source defined)`, async () => {
mockProject(
{
'./src/declarationOfMyNumber.js': `
const myNumber = 3;
export { myNumber };
`,
'./currentFile.js': `
import { myNumber } from './src/declarationOfMyNumber.js';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'MyClass' in the code above
const source = './src/declarationOfMyNumber.js';
const identifierName = 'myNumber';
const currentFilePath = '/my/project/currentFile.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './src/declarationOfMyNumber.js',
specifier: 'myNumber',
});
});
it(`works with multiple re-exports in a file`, async () => {
mockProject(
{
'./packages/accordion/IngAccordionContent.js': `export class IngAccordionContent { }`,
'./packages/accordion/IngAccordionInvokerButton.js': `export class IngAccordionInvokerButton { }`,
'./packages/accordion/index.js': `
export { IngAccordionContent } from './IngAccordionContent.js';
export { IngAccordionInvokerButton } from './IngAccordionInvokerButton.js';`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
// Let's say we want to track down 'IngAccordionInvokerButton' in the code above
const source = './IngAccordionContent.js';
const identifierName = 'IngAccordionContent';
const currentFilePath = '/my/project/packages/accordion/index.js';
const rootPath = '/my/project';
const rootFile = await trackDownIdentifier(source, identifierName, currentFilePath, rootPath);
expect(rootFile).to.deep.equal({
file: './packages/accordion/IngAccordionContent.js',
specifier: 'IngAccordionContent',
});
// Let's say we want to track down 'IngAccordionInvokerButton' in the code above
const source2 = './IngAccordionInvokerButton.js';
const identifierName2 = 'IngAccordionInvokerButton';
const currentFilePath2 = '/my/project/packages/accordion/index.js';
const rootPath2 = '/my/project';
const rootFile2 = await trackDownIdentifier(
source2,
identifierName2,
currentFilePath2,
rootPath2,
);
expect(rootFile2).to.deep.equal({
file: './packages/accordion/IngAccordionInvokerButton.js',
specifier: 'IngAccordionInvokerButton',
});
});
});
describe('trackDownIdentifierFromScope', () => {
it(`gives back [current] if currentFilePath contains declaration`, async () => {
const projectFiles = {
'./src/declarationOfMyClass.js': `
export class MyClass extends HTMLElement {}
`,
};
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
// const ast = AstService._getBabelAst(projectFiles['./src/declarationOfMyClass.js']);
const ast = AstService._getSwcAst(projectFiles['./src/declarationOfMyClass.js']);
// Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'MyClass';
const fullCurrentFilePath = '/my/project//src/declarationOfMyClass.js';
const projectPath = '/my/project';
/** @type {NodePath} */
let astPath;
// babelTraverse.default(ast, {
// ClassDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ClassDeclaration(path) {
astPath = path;
},
});
const rootFile = await trackDownIdentifierFromScope(
// @ts-ignore
astPath,
identifierNameInScope,
fullCurrentFilePath,
projectPath,
);
expect(rootFile).to.deep.equal({
file: '[current]',
specifier: 'MyClass',
});
});
it(`tracks down re-exported identifiers`, async () => {
const projectFiles = {
'./src/declarationOfMyClass.js': `
export class MyClass extends HTMLElement {}
`,
'./re-export.js': `
// 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
export { MyClass } from './src/declarationOfMyClass.js';
`,
'./imported.js': `
import { MyClass } from './re-export.js';
`,
};
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
// const ast = AstService._getBabelAst(projectFiles['./imported.js']);
const ast = AstService._getSwcAst(projectFiles['./imported.js']);
// Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'MyClass';
const fullCurrentFilePath = '/my/project/internal.js';
const projectPath = '/my/project';
/** @type {NodePath} */
let astPath;
// babelTraverse.default(ast, {
// ImportDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ImportDeclaration(path) {
astPath = path;
},
});
const rootFile = await trackDownIdentifierFromScope(
// @ts-ignore
astPath,
identifierNameInScope,
fullCurrentFilePath,
projectPath,
);
expect(rootFile).to.deep.equal({
file: './src/declarationOfMyClass.js',
specifier: 'MyClass',
});
});
it(`tracks down extended classes from a re-export`, async () => {
const projectFiles = {
'./src/classes.js': `
export class El1 extends HTMLElement {}
export class El2 extends HTMLElement {}
`,
'./imported.js': `
export { El1, El2 } from './src/classes.js';
export class ExtendedEl1 extends El1 {}
`,
};
mockProject(projectFiles, { projectName: 'my-project', projectPath: '/my/project' });
// const ast = AstService._getBabelAst(projectFiles['./imported.js']);
const ast = AstService._getSwcAst(projectFiles['./imported.js']);
// Let's say we want to track down 'MyClass' in the code above
const identifierNameInScope = 'El1';
const fullCurrentFilePath = '/my/project/internal.js';
const projectPath = '/my/project';
/** @type {NodePath} */
let astPath;
// babelTraverse.default(ast, {
// ClassDeclaration(path) {
// astPath = path;
// },
// });
swcTraverse(ast, {
ClassDeclaration(path) {
astPath = path;
},
});
const rootFile = await trackDownIdentifierFromScope(
// @ts-ignore
astPath,
identifierNameInScope,
fullCurrentFilePath,
projectPath,
);
expect(rootFile).to.deep.equal({
file: './src/classes.js',
specifier: 'El1',
});
});
});