import { expect } from 'chai'; import { it } from 'mocha'; import { swcTraverse } from '../../../../src/program/utils/swc-traverse.js'; import { trackDownIdentifier, trackDownIdentifierFromScope, } from '../../../../src/program/analyzers/helpers/track-down-identifier.js'; import { AstService } from '../../../../src/program/core/AstService.js'; import { mockProject } from '../../../../test-helpers/mock-project-helpers.js'; import { setupAnalyzerTest } from '../../../../test-helpers/setup-analyzer-test.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', }); }); });