refactor(@schematics/angular): add utility to find top-level identifiers

Adds a utility that will find if a source file has a top-level identifier with a certain name. This will be useful when trying to avoid conflicts in generated imports later on.
This commit is contained in:
Kristiyan Kostadinov 2023-06-01 10:29:56 +02:00 committed by Alan Agius
parent 90e69badcc
commit b36effda51
2 changed files with 120 additions and 0 deletions

View File

@ -665,3 +665,52 @@ export function addRouteDeclarationToModule(
return new InsertChange(fileToAdd, insertPos, route);
}
/** Asserts if the specified node is a named declaration (e.g. class, interface). */
function isNamedNode(
node: ts.Node & { name?: ts.Node },
): node is ts.Node & { name: ts.Identifier } {
return !!node.name && ts.isIdentifier(node.name);
}
/**
* Determines if a SourceFile has a top-level declaration whose name matches a specific symbol.
* Can be used to avoid conflicts when inserting new imports into a file.
* @param sourceFile File in which to search.
* @param symbolName Name of the symbol to search for.
* @param skipModule Path of the module that the symbol may have been imported from. Used to
* avoid false positives where the same symbol we're looking for may have been imported.
*/
export function hasTopLevelIdentifier(
sourceFile: ts.SourceFile,
symbolName: string,
skipModule: string | null = null,
): boolean {
for (const node of sourceFile.statements) {
if (isNamedNode(node) && node.name.text === symbolName) {
return true;
}
if (
ts.isVariableStatement(node) &&
node.declarationList.declarations.some((decl) => {
return isNamedNode(decl) && decl.name.text === symbolName;
})
) {
return true;
}
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteralLike(node.moduleSpecifier) &&
node.moduleSpecifier.text !== skipModule &&
node.importClause?.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings) &&
node.importClause.namedBindings.elements.some((el) => el.name.text === symbolName)
) {
return true;
}
}
return false;
}

View File

@ -18,6 +18,7 @@ import {
addRouteDeclarationToModule,
addSymbolToNgModuleMetadata,
findNodes,
hasTopLevelIdentifier,
insertAfterLastOccurrence,
insertImport,
} from './ast-utils';
@ -776,4 +777,74 @@ describe('ast utils', () => {
expect(result).toBe(fileContent);
});
});
describe('hasTopLevelIdentifier', () => {
const filePath = './src/foo.ts';
it('should find top-level class declaration with a specific name', () => {
const fileContent = `class FooClass {}`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'FooClass')).toBe(true);
expect(hasTopLevelIdentifier(source, 'Foo')).toBe(false);
});
it('should find top-level interface declaration with a specific name', () => {
const fileContent = `interface FooInterface {}`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'FooInterface')).toBe(true);
expect(hasTopLevelIdentifier(source, 'Foo')).toBe(false);
});
it('should find top-level variable declaration with a specific name', () => {
const fileContent = `
const singleVar = 1;
const fooVar = 1, barVar = 2;
`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'singleVar')).toBe(true);
expect(hasTopLevelIdentifier(source, 'fooVar')).toBe(true);
expect(hasTopLevelIdentifier(source, 'barVar')).toBe(true);
expect(hasTopLevelIdentifier(source, 'bar')).toBe(false);
});
it('should find top-level imports with a specific name', () => {
const fileContent = `
import { FooInterface } from '@foo/interfaces';
class FooClass implements FooInterface {}
`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'FooInterface')).toBe(true);
expect(hasTopLevelIdentifier(source, 'Foo')).toBe(false);
});
it('should find top-level aliased imports with a specific name', () => {
const fileContent = `
import { FooInterface as AliasedFooInterface } from '@foo/interfaces';
class FooClass implements AliasedFooInterface {}
`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'AliasedFooInterface')).toBe(true);
expect(hasTopLevelIdentifier(source, 'FooInterface')).toBe(false);
expect(hasTopLevelIdentifier(source, 'Foo')).toBe(false);
});
it('should be able to skip imports from a certain module', () => {
const fileContent = `
import { FooInterface } from '@foo/interfaces';
class FooClass implements FooInterface {}
`;
const source = getTsSource(filePath, fileContent);
expect(hasTopLevelIdentifier(source, 'FooInterface', '@foo/interfaces')).toBe(false);
});
});
});