mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(language-service): support definitions for selectorless (#61240)
Updates the language service to handle producing definition information for selectorless components and directives. PR Close #61240
This commit is contained in:
parent
c69dda61c2
commit
cec512fdfd
4 changed files with 286 additions and 2 deletions
|
|
@ -22,6 +22,8 @@ import {
|
|||
DirectiveSymbol,
|
||||
DomBindingSymbol,
|
||||
ElementSymbol,
|
||||
SelectorlessComponentSymbol,
|
||||
SelectorlessDirectiveSymbol,
|
||||
Symbol,
|
||||
SymbolKind,
|
||||
TcbLocation,
|
||||
|
|
@ -118,6 +120,8 @@ export class DefinitionBuilder {
|
|||
case SymbolKind.Element:
|
||||
case SymbolKind.Template:
|
||||
case SymbolKind.DomBinding:
|
||||
case SymbolKind.SelectorlessComponent:
|
||||
case SymbolKind.SelectorlessDirective:
|
||||
// Though it is generally more appropriate for the above symbol definitions to be
|
||||
// associated with "type definitions" since the location in the template is the
|
||||
// actual definition location, the better user experience would be to allow
|
||||
|
|
@ -238,6 +242,8 @@ export class DefinitionBuilder {
|
|||
case SymbolKind.DomBinding:
|
||||
case SymbolKind.Element:
|
||||
case SymbolKind.Template:
|
||||
case SymbolKind.SelectorlessComponent:
|
||||
case SymbolKind.SelectorlessDirective:
|
||||
definitions.push(...this.getTypeDefinitionsForTemplateInstance(symbol, node));
|
||||
break;
|
||||
case SymbolKind.Output:
|
||||
|
|
@ -286,7 +292,13 @@ export class DefinitionBuilder {
|
|||
}
|
||||
|
||||
private getTypeDefinitionsForTemplateInstance(
|
||||
symbol: TemplateSymbol | ElementSymbol | DomBindingSymbol | DirectiveSymbol,
|
||||
symbol:
|
||||
| TemplateSymbol
|
||||
| ElementSymbol
|
||||
| DomBindingSymbol
|
||||
| DirectiveSymbol
|
||||
| SelectorlessComponentSymbol
|
||||
| SelectorlessDirectiveSymbol,
|
||||
node: AST | TmplAstNode,
|
||||
): ts.DefinitionInfo[] {
|
||||
switch (symbol.kind) {
|
||||
|
|
@ -313,6 +325,8 @@ export class DefinitionBuilder {
|
|||
);
|
||||
return this.getTypeDefinitionsForSymbols(...dirs);
|
||||
}
|
||||
case SymbolKind.SelectorlessComponent:
|
||||
case SymbolKind.SelectorlessDirective:
|
||||
case SymbolKind.Directive:
|
||||
return this.getTypeDefinitionsForSymbols(symbol);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
assertFileNames,
|
||||
assertTextSpans,
|
||||
createModuleAndProjectWithDeclarations,
|
||||
createProjectWithStandaloneDeclarations,
|
||||
humanizeDocumentSpanLike,
|
||||
LanguageServiceTestEnv,
|
||||
OpenBuffer,
|
||||
|
|
@ -472,6 +473,252 @@ describe('definitions', () => {
|
|||
assertFileNames(Array.from(definitions), ['app.html']);
|
||||
});
|
||||
|
||||
it('gets definition for selectorless component', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({template: ''})
|
||||
export class Dep {}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<Dep/>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('<De¦p/>');
|
||||
const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
expect(definitions[0].name).toEqual('Dep');
|
||||
expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
|
||||
expect(definitions[0].textSpan).toBe('Dep');
|
||||
assertFileNames(Array.from(definitions), ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition for selectorless directive', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Dep {}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<div @Dep></div>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('@De¦p');
|
||||
const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
expect(definitions[0].name).toEqual('Dep');
|
||||
expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
|
||||
expect(definitions[0].textSpan).toBe('Dep');
|
||||
assertFileNames(Array.from(definitions), ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition of selectorless component input', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({template: ''})
|
||||
export class Dep {
|
||||
@Input() someInput: any;
|
||||
}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<Dep [someInput]="123"/>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('[some¦Input]');
|
||||
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
|
||||
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
|
||||
'someInput',
|
||||
);
|
||||
expect(definitions.length).toBe(1);
|
||||
expect(definitions[0].textSpan).toContain('someInput');
|
||||
assertFileNames(definitions, ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition of selectorless directive input', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Directive, Input} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Dep {
|
||||
@Input() someInput: any;
|
||||
}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<div @Dep([someInput]="123")></div>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('[some¦Input]');
|
||||
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
|
||||
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
|
||||
'someInput',
|
||||
);
|
||||
expect(definitions.length).toBe(1);
|
||||
expect(definitions[0].textSpan).toContain('someInput');
|
||||
assertFileNames(definitions, ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition of selectorless component output', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Component, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@Component({template: ''})
|
||||
export class Dep {
|
||||
@Output() someEvent = new EventEmitter<void>();
|
||||
}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {
|
||||
handler() {}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<Dep (someEvent)="handler()"/>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('(some¦Event)');
|
||||
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
|
||||
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
|
||||
'someEvent',
|
||||
);
|
||||
expect(definitions.length).toBe(1);
|
||||
expect(definitions[0].textSpan).toContain('someEvent');
|
||||
assertFileNames(definitions, ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition of selectorless directive output', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
'app.html': '',
|
||||
'dep.ts': `
|
||||
import {Directive, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Dep {
|
||||
@Output() someEvent = new EventEmitter<void>();
|
||||
}
|
||||
`,
|
||||
'app.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
import {Dep} from './dep';
|
||||
|
||||
@Component({
|
||||
templateUrl: '/app.html',
|
||||
})
|
||||
export class AppCmp {
|
||||
handler() {}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const env = LanguageServiceTestEnv.setup();
|
||||
|
||||
const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
|
||||
_enableSelectorless: true,
|
||||
});
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<div @Dep((someEvent)="handler()")></div>';
|
||||
project.expectNoSourceDiagnostics();
|
||||
|
||||
template.moveCursorToText('(some¦Event)');
|
||||
const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
|
||||
|
||||
expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
|
||||
'someEvent',
|
||||
);
|
||||
expect(definitions.length).toBe(1);
|
||||
expect(definitions[0].textSpan).toContain('someEvent');
|
||||
assertFileNames(definitions, ['dep.ts']);
|
||||
});
|
||||
|
||||
it('gets definition for a method in a void expression', () => {
|
||||
initMockFileSystem('Native');
|
||||
const files = {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ function writeTsconfig(
|
|||
|
||||
export type TestableOptions = TypeCheckingOptions &
|
||||
InternalOptions &
|
||||
Pick<LegacyNgcOptions, 'fullTemplateTypeCheck'>;
|
||||
Pick<LegacyNgcOptions, 'fullTemplateTypeCheck'> & {
|
||||
// This already exists in `InternalOptions`, but it's `internal` so it's stripped away.
|
||||
_enableSelectorless?: boolean;
|
||||
};
|
||||
|
||||
export class Project {
|
||||
private tsProject: ts.server.Project;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,26 @@ export function createModuleAndProjectWithDeclarations(
|
|||
return env.addProject(projectName, {...projectFiles, ...standaloneFiles}, angularCompilerOptions);
|
||||
}
|
||||
|
||||
export function createProjectWithStandaloneDeclarations(
|
||||
env: LanguageServiceTestEnv,
|
||||
projectName: string,
|
||||
projectFiles: ProjectFiles,
|
||||
angularCompilerOptions: TestableOptions = {},
|
||||
standaloneFiles: ProjectFiles = {},
|
||||
): Project {
|
||||
const externalClasses: string[] = [];
|
||||
const externalImports: string[] = [];
|
||||
for (const [fileName, fileContents] of Object.entries(projectFiles)) {
|
||||
if (!fileName.endsWith('.ts')) {
|
||||
continue;
|
||||
}
|
||||
const className = getFirstClassDeclaration(fileContents);
|
||||
externalClasses.push(className);
|
||||
externalImports.push(`import {${className}} from './${fileName.replace('.ts', '')}';`);
|
||||
}
|
||||
return env.addProject(projectName, {...projectFiles, ...standaloneFiles}, angularCompilerOptions);
|
||||
}
|
||||
|
||||
export function humanizeDocumentSpanLike<T extends ts.DocumentSpan>(
|
||||
item: T,
|
||||
env: LanguageServiceTestEnv,
|
||||
|
|
|
|||
Loading…
Reference in a new issue