lion/packages-node/providence-analytics/test-node/program/analyzers/match-subclasses.test.js
2023-11-08 19:01:20 +01:00

361 lines
12 KiB
JavaScript

const { expect } = require('chai');
const { providence } = require('../../../src/program/providence.js');
const { QueryService } = require('../../../src/program/core/QueryService.js');
const { mockTargetAndReferenceProject } = require('../../../test-helpers/mock-project-helpers.js');
const { setupAnalyzerTest } = require('../../../test-helpers/setup-analyzer-test.js');
// 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 = QueryService.getQueryConfigFromAnalyzer('match-subclasses');
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' },
// ...
],
},
// ...
],
},
];
// eslint-disable-next-line no-shadow
describe('Analyzer "match-subclasses"', () => {
const queryResults = setupAnalyzerTest();
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);
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);
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);
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);
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);
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);
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' },
]);
});
});
});
});