angular/packages/language-service/test/ts_utils_spec.ts
2025-11-06 13:09:01 -08:00

241 lines
8.1 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import ts from 'typescript';
import {
addElementToArrayLiteral,
collectMemberMethods,
ensureArrayWithIdentifier,
findTightestNode,
generateImport,
hasImport,
nonCollidingImportName,
objectPropertyAssignmentForKey,
printNode,
updateImport,
updateObjectValueForKey,
} from '../src/utils/ts_utils';
import {LanguageServiceTestEnv, OpenBuffer, Project} from '../testing';
describe('TS util', () => {
describe('collectMemberMethods', () => {
beforeEach(() => {
initMockFileSystem('Native');
});
it('gets only methods in class, not getters, setters, or properties', () => {
const files = {
'app.ts': `
export class AppCmp {
prop!: string;
get myString(): string {
return '';
}
set myString(v: string) {
}
one() {}
two() {}
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const appFile = project.openFile('app.ts');
appFile.moveCursorToText('AppC¦mp');
const memberMethods = getMemberMethodNames(project, appFile);
expect(memberMethods).toEqual(['one', 'two']);
});
it('gets inherited methods in class', () => {
const files = {
'app.ts': `
export class BaseClass {
baseMethod() {}
}
export class AppCmp extends BaseClass {}`,
};
const env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const appFile = project.openFile('app.ts');
appFile.moveCursorToText('AppC¦mp');
const memberMethods = getMemberMethodNames(project, appFile);
expect(memberMethods).toEqual(['baseMethod']);
});
it('does not return duplicates if base method is overridden', () => {
const files = {
'app.ts': `
export class BaseClass {
baseMethod() {}
}
export class AppCmp extends BaseClass {
baseMethod() {}
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = env.addProject('test', files);
const appFile = project.openFile('app.ts');
appFile.moveCursorToText('AppC¦mp');
const memberMethods = getMemberMethodNames(project, appFile);
expect(memberMethods).toEqual(['baseMethod']);
});
function getMemberMethodNames(project: Project, file: OpenBuffer): string[] {
const sf = project.getSourceFile('app.ts')!;
const node = findTightestNode(sf, file.cursor)!;
expect(ts.isClassDeclaration(node.parent)).toBe(true);
return collectMemberMethods(node.parent as ts.ClassDeclaration, project.getTypeChecker())
.map((m) => m.name.getText())
.sort();
}
});
describe('AST method', () => {
let printer: ts.Printer;
let sourceFile: ts.SourceFile;
function print(node: ts.Node): string {
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
}
beforeAll(() => {
printer = ts.createPrinter();
sourceFile = ts.createSourceFile(
'placeholder.ts',
'',
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS,
);
});
describe('addElementToArrayLiteral', () => {
it('transforms an empty array literal expression', () => {
const oldArr = ts.factory.createArrayLiteralExpression([], false);
const newArr = addElementToArrayLiteral(oldArr, ts.factory.createStringLiteral('a'));
expect(print(newArr)).toEqual('["a"]');
});
it('transforms an existing array literal expression', () => {
const oldArr = ts.factory.createArrayLiteralExpression(
[ts.factory.createStringLiteral('a')],
false,
);
const newArr = addElementToArrayLiteral(oldArr, ts.factory.createStringLiteral('b'));
expect(print(newArr)).toEqual('["a", "b"]');
});
});
describe('objectPropertyAssignmentForKey', () => {
let oldObj: ts.ObjectLiteralExpression;
beforeEach(() => {
oldObj = ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('foo'),
ts.factory.createStringLiteral('bar'),
),
],
false,
);
});
it('returns null when no property exists', () => {
const prop = objectPropertyAssignmentForKey(oldObj, 'oops');
expect(prop).toBeNull();
});
it('returns the requested property assignment', () => {
const prop = objectPropertyAssignmentForKey(oldObj, 'foo');
expect(print(prop!)).toEqual('foo: "bar"');
});
});
describe('updateObjectValueForKey', () => {
let oldObj: ts.ObjectLiteralExpression;
const valueAppenderFn = (oldValue?: ts.Expression) => {
if (!oldValue) return ts.factory.createStringLiteral('baz');
if (!ts.isStringLiteral(oldValue)) return oldValue;
return ts.factory.createStringLiteral(oldValue.text + 'baz');
};
beforeEach(() => {
oldObj = ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('foo'),
ts.factory.createStringLiteral('bar'),
),
],
false,
);
});
it('creates a non-existent property', () => {
const obj = updateObjectValueForKey(oldObj, 'newKey', valueAppenderFn);
expect(print(obj)).toBe('newKey: "baz"');
});
it('updates an existing property', () => {
const obj = updateObjectValueForKey(oldObj, 'foo', valueAppenderFn);
expect(print(obj)).toBe('foo: "barbaz"');
});
});
it('addElementToArrayLiteral', () => {
let arr = ensureArrayWithIdentifier('foo', ts.factory.createIdentifier('foo'));
arr = addElementToArrayLiteral(arr!, ts.factory.createIdentifier('bar'));
expect(print(arr)).toEqual('[foo, bar]');
});
it('ensureArrayWithIdentifier', () => {
let arr = ensureArrayWithIdentifier('foo', ts.factory.createIdentifier('foo'));
expect(print(arr!)).toEqual('[foo]');
arr = ensureArrayWithIdentifier('bar', ts.factory.createIdentifier('bar'), arr!);
expect(print(arr!)).toEqual('[foo, bar]');
arr = ensureArrayWithIdentifier('bar', ts.factory.createIdentifier('bar'), arr!);
expect(arr).toEqual(null);
});
it('generateImport', () => {
let imp = generateImport('Foo', null, './foo');
expect(print(imp)).toEqual(`import { Foo } from "./foo";`);
imp = generateImport('Foo', 'Bar', './foo');
expect(print(imp)).toEqual(`import { Bar as Foo } from "./foo";`);
});
it('updateImport', () => {
let imp = generateImport('Foo', null, './foo');
let namedImp = updateImport(imp, 'Bar', null);
expect(print(namedImp!)).toEqual(`{ Foo, Bar }`);
namedImp = updateImport(imp, 'Foo_2', 'Foo');
expect(print(namedImp!)).toEqual(`{ Foo, Foo as Foo_2 }`);
namedImp = updateImport(imp, 'Bar', 'Bar');
expect(print(namedImp!)).toEqual(`{ Foo, Bar }`);
namedImp = updateImport(imp, 'default', 'Baz');
expect(print(namedImp!)).toEqual(`Baz, { Foo }`);
imp = generateImport('default', 'Foo', './foo');
namedImp = updateImport(imp, 'Bar', null);
expect(print(namedImp!)).toEqual(`Foo, { Bar }`);
});
it('nonCollidingImportName', () => {
let imps = [
generateImport('Foo', null, './foo'),
generateImport('Bar', 'ExternalBar', './bar'),
];
expect(nonCollidingImportName(imps, 'Other')).toEqual('Other');
expect(nonCollidingImportName(imps, 'Foo')).toEqual('Foo_1');
expect(nonCollidingImportName(imps, 'ExternalBar')).toEqual('ExternalBar');
});
});
});