mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 10:33:43 +08:00
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:
parent
90e69badcc
commit
b36effda51
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user