368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
import { expect } from 'chai';
|
|
import { it } from 'mocha';
|
|
import { providence } from '../../../src/program/providence.js';
|
|
import { QueryService } from '../../../src/program/core/QueryService.js';
|
|
import { mockTargetAndReferenceProject } from '../../../test-helpers/mock-project-helpers.js';
|
|
import { setupAnalyzerTest } from '../../../test-helpers/setup-analyzer-test.js';
|
|
import MatchSubclassesAnalyzer from '../../../src/program/analyzers/match-subclasses.js';
|
|
|
|
/**
|
|
* @typedef {import('../../../types/index.js').ProvidenceConfig} ProvidenceConfig
|
|
*/
|
|
|
|
setupAnalyzerTest();
|
|
|
|
describe('Analyzer "match-subclasses"', async () => {
|
|
// 1. Reference input data
|
|
const referenceProject = {
|
|
path: '/importing/target/project/node_modules/exporting-ref-project',
|
|
name: 'exporting-ref-project',
|
|
files: [
|
|
// This file contains all 'original' exported definitions
|
|
{
|
|
file: './ref-src/core.js',
|
|
code: `
|
|
// named specifier
|
|
export class RefClass extends HTMLElement {};
|
|
|
|
// default specifier
|
|
export default class OtherClass {};
|
|
`,
|
|
},
|
|
// This file is used to test file system 'resolvements' -> importing repos using
|
|
// `import 'exporting-ref-project/ref-src/folder'` should be pointed to this index.js file
|
|
{
|
|
file: './index.js',
|
|
code: `
|
|
export { RefClass as RefRenamedClass } from './ref-src/core.js';
|
|
|
|
// re-exported default specifier
|
|
import refConstImported from './ref-src/core.js';
|
|
export default refConstImported;
|
|
|
|
export const Mixin = superclass => class MyMixin extends superclass {}
|
|
`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const searchTargetProject = {
|
|
path: '/importing/target/project',
|
|
name: 'importing-target-project',
|
|
files: [
|
|
{
|
|
file: './target-src/indirect-imports.js',
|
|
// Indirect (via project root) imports
|
|
code: `
|
|
// renamed import (indirect, needs transitivity check)
|
|
import { RefRenamedClass } from 'exporting-ref-project';
|
|
import defaultExport from 'exporting-ref-project';
|
|
|
|
class ExtendRefRenamedClass extends RefRenamedClass {}
|
|
`,
|
|
},
|
|
{
|
|
file: './target-src/direct-imports.js',
|
|
code: `
|
|
// a direct named import
|
|
import { RefClass } from 'exporting-ref-project/ref-src/core.js';
|
|
|
|
// a direct default import
|
|
import RefDefault from 'exporting-ref-project';
|
|
|
|
// a direct named mixin
|
|
import { Mixin } from 'exporting-ref-project';
|
|
|
|
// Non match
|
|
import { ForeignMixin } from 'unknow-project';
|
|
|
|
class ExtendRefClass extends RefClass {}
|
|
class ExtendRefDefault extends RefDefault {}
|
|
class ExtendRefClassWithMixin extends ForeignMixin(Mixin(RefClass)) {}
|
|
`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const matchSubclassesQueryConfig = await QueryService.getQueryConfigFromAnalyzer(
|
|
MatchSubclassesAnalyzer,
|
|
);
|
|
/** @type {Partial<ProvidenceConfig>} */
|
|
const _providenceCfg = {
|
|
targetProjectPaths: [searchTargetProject.path],
|
|
referenceProjectPaths: [referenceProject.path],
|
|
};
|
|
|
|
// 2. Extracted specifiers (by find-exports analyzer)
|
|
const expectedExportIdsIndirect = ['RefRenamedClass::./index.js::exporting-ref-project'];
|
|
|
|
const expectedExportIdsDirect = [
|
|
// ids should be unique across multiple projects
|
|
// Not in scope: version number of a project.
|
|
'RefClass::./ref-src/core.js::exporting-ref-project',
|
|
'[default]::./index.js::exporting-ref-project',
|
|
'Mixin::./index.js::exporting-ref-project',
|
|
];
|
|
// eslint-disable-next-line no-unused-vars
|
|
const expectedExportIds = [...expectedExportIdsIndirect, ...expectedExportIdsDirect];
|
|
|
|
// 3. The AnalyzerQueryResult generated by "match-subclasses"
|
|
// eslint-disable-next-line no-unused-vars
|
|
const expectedMatchesOutput = [
|
|
{
|
|
exportSpecifier: {
|
|
name: 'RefClass',
|
|
// name under which it is registered in npm ("name" attr in package.json)
|
|
project: 'exporting-ref-project',
|
|
filePath: './ref-src/core.js',
|
|
id: 'RefClass::./ref-src/core.js::exporting-ref-project',
|
|
|
|
// TODO: next step => identify transitive relations and add inside
|
|
// most likely via post processor
|
|
},
|
|
// All the matched targets (files importing the specifier), ordered per project
|
|
matchesPerProject: [
|
|
{
|
|
project: 'importing-target-project',
|
|
files: [
|
|
{ file: './target-src/indirect-imports.js', identifier: 'ExtendedRefClass' },
|
|
// ...
|
|
],
|
|
},
|
|
// ...
|
|
],
|
|
},
|
|
];
|
|
|
|
describe('Match Features', () => {
|
|
it(`identifies all directly imported class extensions`, async () => {
|
|
const refProject = {
|
|
path: '/target/node_modules/ref',
|
|
name: 'ref',
|
|
files: [{ file: './LionComp.js', code: `export class LionComp extends HTMLElement {};` }],
|
|
};
|
|
const targetProject = {
|
|
path: '/target',
|
|
name: 'target',
|
|
files: [
|
|
{
|
|
file: './WolfComp.js',
|
|
code: `
|
|
import { LionComp } from 'ref/LionComp.js';
|
|
|
|
export class WolfComp extends LionComp {}
|
|
`,
|
|
},
|
|
],
|
|
};
|
|
mockTargetAndReferenceProject(targetProject, refProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, {
|
|
targetProjectPaths: [targetProject.path],
|
|
referenceProjectPaths: [refProject.path],
|
|
});
|
|
const queryResult = queryResults[0];
|
|
expect(queryResult.queryOutput).eql([
|
|
{
|
|
exportSpecifier: {
|
|
filePath: './LionComp.js',
|
|
id: 'LionComp::./LionComp.js::ref',
|
|
name: 'LionComp',
|
|
project: 'ref',
|
|
},
|
|
matchesPerProject: [
|
|
{
|
|
files: [
|
|
{ file: './WolfComp.js', identifier: 'WolfComp', memberOverrides: undefined },
|
|
],
|
|
project: 'target',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it(`identifies all indirectly imported (transitive) class extensions`, async () => {
|
|
const refProject = {
|
|
path: '/target/node_modules/ref',
|
|
name: 'ref',
|
|
files: [
|
|
{ file: './LionComp.js', code: `export class LionComp extends HTMLElement {};` },
|
|
{
|
|
file: './RenamedLionComp.js',
|
|
code: `export { LionComp as RenamedLionComp } from './LionComp.js';`,
|
|
},
|
|
],
|
|
};
|
|
const targetProject = {
|
|
path: '/target',
|
|
name: 'target',
|
|
files: [
|
|
{
|
|
file: './WolfComp2.js',
|
|
code: `
|
|
import { RenamedLionComp } from 'ref/RenamedLionComp.js';
|
|
|
|
export class WolfComp2 extends RenamedLionComp {}
|
|
`,
|
|
},
|
|
],
|
|
};
|
|
mockTargetAndReferenceProject(targetProject, refProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, {
|
|
targetProjectPaths: [targetProject.path],
|
|
referenceProjectPaths: [refProject.path],
|
|
});
|
|
const queryResult = queryResults[0];
|
|
expect(queryResult.queryOutput).eql([
|
|
{
|
|
exportSpecifier: {
|
|
filePath: './RenamedLionComp.js',
|
|
id: 'RenamedLionComp::./RenamedLionComp.js::ref',
|
|
name: 'RenamedLionComp',
|
|
project: 'ref',
|
|
},
|
|
matchesPerProject: [
|
|
{
|
|
files: [
|
|
{ file: './WolfComp2.js', identifier: 'WolfComp2', memberOverrides: undefined },
|
|
],
|
|
project: 'target',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it(`identifies Mixins`, async () => {
|
|
const refProject = {
|
|
path: '/target/node_modules/ref',
|
|
name: 'ref',
|
|
files: [
|
|
{
|
|
file: './LionMixin.js',
|
|
code: `
|
|
export function LionMixin(superclass) {
|
|
return class extends superclass {};
|
|
}`,
|
|
},
|
|
],
|
|
};
|
|
const targetProject = {
|
|
path: '/target',
|
|
name: 'target',
|
|
files: [
|
|
{
|
|
file: './WolfCompUsingMixin.js',
|
|
code: `
|
|
import { LionMixin } from 'ref/LionMixin.js';
|
|
|
|
export class WolfCompUsingMixin extends LionMixin(HTMLElement) {}
|
|
`,
|
|
},
|
|
],
|
|
};
|
|
mockTargetAndReferenceProject(targetProject, refProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, {
|
|
targetProjectPaths: [targetProject.path],
|
|
referenceProjectPaths: [refProject.path],
|
|
});
|
|
const queryResult = queryResults[0];
|
|
expect(queryResult.queryOutput).eql([
|
|
{
|
|
exportSpecifier: {
|
|
filePath: './LionMixin.js',
|
|
id: 'LionMixin::./LionMixin.js::ref',
|
|
name: 'LionMixin',
|
|
project: 'ref',
|
|
},
|
|
matchesPerProject: [
|
|
{
|
|
files: [
|
|
{
|
|
file: './WolfCompUsingMixin.js',
|
|
identifier: 'WolfCompUsingMixin',
|
|
memberOverrides: undefined,
|
|
},
|
|
],
|
|
project: 'target',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('Extracting exports', () => {
|
|
describe('Inside small example project', () => {
|
|
it(`identifies all indirect export specifiers consumed by "importing-target-project"`, async () => {
|
|
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, _providenceCfg);
|
|
const queryResult = queryResults[0];
|
|
expectedExportIdsIndirect.forEach(indirectId => {
|
|
expect(
|
|
queryResult.queryOutput.find(
|
|
exportMatchResult => exportMatchResult.exportSpecifier.id === indirectId,
|
|
),
|
|
).not.to.equal(undefined, `id '${indirectId}' not found`);
|
|
});
|
|
});
|
|
|
|
it(`identifies all direct export specifiers consumed by "importing-target-project"`, async () => {
|
|
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, _providenceCfg);
|
|
const queryResult = queryResults[0];
|
|
expectedExportIdsDirect.forEach(directId => {
|
|
expect(
|
|
queryResult.queryOutput.find(
|
|
exportMatchResult => exportMatchResult.exportSpecifier.id === directId,
|
|
),
|
|
).not.to.equal(undefined, `id '${directId}' not found`);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Matching', () => {
|
|
// TODO: because we intoduced an object in match-classes, we find duplicate entries in
|
|
// our result set cretaed in macth-subclasses. Fix there...
|
|
it.skip(`produces a list of all matches, sorted by project`, async () => {
|
|
function testMatchedEntry(targetExportedId, queryResult, importedByFiles = []) {
|
|
const matchedEntry = queryResult.queryOutput.find(
|
|
r => r.exportSpecifier.id === targetExportedId,
|
|
);
|
|
|
|
const [name, filePath, project] = targetExportedId.split('::');
|
|
expect(matchedEntry.exportSpecifier).to.eql({
|
|
name,
|
|
filePath,
|
|
project,
|
|
id: targetExportedId,
|
|
});
|
|
expect(matchedEntry.matchesPerProject[0].project).to.equal('importing-target-project');
|
|
expect(matchedEntry.matchesPerProject[0].files).to.eql(importedByFiles);
|
|
}
|
|
|
|
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
|
|
const queryResults = await providence(matchSubclassesQueryConfig, _providenceCfg);
|
|
const queryResult = queryResults[0];
|
|
|
|
expectedExportIdsDirect.forEach(targetId => {
|
|
testMatchedEntry(targetId, queryResult, [
|
|
// TODO: 'identifier' needs to be the exported name of extending class
|
|
{
|
|
identifier: targetId.split('::')[0],
|
|
file: './target-src/direct-imports.js',
|
|
memberOverrides: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
expectedExportIdsIndirect.forEach(targetId => {
|
|
testMatchedEntry(targetId, queryResult, [
|
|
// TODO: 'identifier' needs to be the exported name of extending class
|
|
{ identifier: targetId.split('::')[0], file: './target-src/indirect-imports.js' },
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|