Projects opened in the LS are often larger in scope than the compilation units seen by the compiler when actually building. For example, in the LS it's not uncommon for the project to include both application as well as test files. This can create issues when the combination of files results in errors that are not otherwise present - for example, if test files have inline NgModules that re-declare components (a common Angular pattern). Such code is valid when compiling the app only (test files are excluded, so only one declaration is seen by the compiler) or when compiling tests only (since tests run in JIT mode and are not seen by the AOT compiler), but when both sets of files are mixed into a single compilation unit, the compiler sees the double declaration as an error. This commit attempts to mitigate the problem by forcing the compiler flag `compileNonExportedClasses` to `false` in a LS context. When tests contain duplicate declarations, often such declarations are inline in specs and not exported from the top level, so this flag can be used to greatly improve the IDE experience. PR Close #40092
189 lines
6.7 KiB
TypeScript
189 lines
6.7 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.io/license
|
|
*/
|
|
|
|
import {absoluteFrom, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
|
|
|
import {LanguageServiceTestEnvironment} from './env';
|
|
|
|
describe('language-service/compiler integration', () => {
|
|
beforeEach(() => {
|
|
initMockFileSystem('Native');
|
|
});
|
|
|
|
it('should not produce errors from inline test declarations mixing with those of the app', () => {
|
|
const appCmpFile = absoluteFrom('/test.cmp.ts');
|
|
const appModuleFile = absoluteFrom('/test.mod.ts');
|
|
const testFile = absoluteFrom('/test_spec.ts');
|
|
|
|
const env = LanguageServiceTestEnvironment.setup([
|
|
{
|
|
name: appCmpFile,
|
|
contents: `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'app-cmp',
|
|
template: 'Some template',
|
|
})
|
|
export class AppCmp {}
|
|
`,
|
|
isRoot: true,
|
|
},
|
|
{
|
|
name: appModuleFile,
|
|
contents: `
|
|
import {NgModule} from '@angular/core';
|
|
import {AppCmp} from './test.cmp';
|
|
|
|
@NgModule({
|
|
declarations: [AppCmp],
|
|
})
|
|
export class AppModule {}
|
|
`,
|
|
isRoot: true,
|
|
},
|
|
{
|
|
name: testFile,
|
|
contents: `
|
|
import {NgModule} from '@angular/core';
|
|
import {AppCmp} from './test.cmp';
|
|
|
|
export function test(): void {
|
|
@NgModule({
|
|
declarations: [AppCmp],
|
|
})
|
|
class TestModule {}
|
|
}
|
|
`,
|
|
isRoot: true,
|
|
}
|
|
]);
|
|
|
|
// Expect that this program is clean diagnostically.
|
|
const ngCompiler = env.ngLS.compilerFactory.getOrCreate();
|
|
const program = ngCompiler.getNextProgram();
|
|
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, appCmpFile))).toEqual([]);
|
|
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, appModuleFile))).toEqual([]);
|
|
expect(ngCompiler.getDiagnostics(getSourceFileOrError(program, testFile))).toEqual([]);
|
|
});
|
|
|
|
it('should show type-checking errors from components with poisoned scopes', () => {
|
|
// Normally, the Angular compiler suppresses errors from components that belong to NgModules
|
|
// which themselves have errors (such scopes are considered "poisoned"), to avoid overwhelming
|
|
// the user with secondary errors that stem from a primary root cause. However, this prevents
|
|
// the generation of type check blocks and other metadata within the compiler which drive the
|
|
// Language Service's understanding of components. Therefore in the Language Service, the
|
|
// compiler is configured to make use of such data even if it's "poisoned". This test verifies
|
|
// that a component declared in an NgModule with a faulty import still generates template
|
|
// diagnostics.
|
|
|
|
const file = absoluteFrom('/test.ts');
|
|
const env = LanguageServiceTestEnvironment.setup([{
|
|
name: file,
|
|
contents: `
|
|
import {Component, Directive, Input, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'test-cmp',
|
|
template: '<div [dir]="3"></div>',
|
|
})
|
|
export class Cmp {}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
})
|
|
export class Dir {
|
|
@Input() dir!: string;
|
|
}
|
|
|
|
export class NotAModule {}
|
|
|
|
@NgModule({
|
|
declarations: [Cmp, Dir],
|
|
imports: [NotAModule],
|
|
})
|
|
export class Mod {}
|
|
`,
|
|
isRoot: true,
|
|
}]);
|
|
|
|
const diags = env.ngLS.getSemanticDiagnostics(file);
|
|
expect(diags.map(diag => diag.messageText))
|
|
.toContain(`Type 'number' is not assignable to type 'string'.`);
|
|
});
|
|
|
|
it('should handle broken imports during incremental build steps', () => {
|
|
// This test validates that the compiler's incremental APIs correctly handle a broken import
|
|
// when invoked via the Language Service. Testing this via the LS is important as only the LS
|
|
// requests Angular analysis in the presence of TypeScript-level errors. In the case of broken
|
|
// imports this distinction is especially important: Angular's incremental analysis is
|
|
// built on the the compiler's dependency graph, and this graph must be able to function even
|
|
// with broken imports.
|
|
//
|
|
// The test works by creating a component/module pair where the module imports and declares a
|
|
// component from a separate file. That component is initially not exported, meaning the
|
|
// module's import is broken. Angular will correctly complain that the NgModule is declaring a
|
|
// value which is not statically analyzable.
|
|
//
|
|
// Then, the component file is fixed to properly export the component class, and an incremental
|
|
// build step is performed. The compiler should recognize that the module's previous analysis
|
|
// is stale, even though it was not able to fully understand the import during the first pass.
|
|
|
|
const moduleFile = absoluteFrom('/mod.ts');
|
|
const componentFile = absoluteFrom('/cmp.ts');
|
|
|
|
const componentSource = (isExported: boolean): string => `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'some-cmp',
|
|
template: 'Not important',
|
|
})
|
|
${isExported ? 'export' : ''} class Cmp {}
|
|
`;
|
|
|
|
const env = LanguageServiceTestEnvironment.setup([
|
|
{
|
|
name: moduleFile,
|
|
contents: `
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
@NgModule({
|
|
declarations: [Cmp],
|
|
})
|
|
export class Mod {}
|
|
`,
|
|
isRoot: true,
|
|
},
|
|
{
|
|
name: componentFile,
|
|
contents: componentSource(/* start with component not exported */ false),
|
|
isRoot: true,
|
|
}
|
|
]);
|
|
|
|
// Angular should be complaining about the module not being understandable.
|
|
const programBefore = env.tsLS.getProgram()!;
|
|
const moduleSfBefore = programBefore.getSourceFile(moduleFile)!;
|
|
const ngDiagsBefore = env.ngLS.compilerFactory.getOrCreate().getDiagnostics(moduleSfBefore);
|
|
expect(ngDiagsBefore.length).toBe(1);
|
|
|
|
// Fix the import.
|
|
env.updateFile(componentFile, componentSource(/* properly export the component */ true));
|
|
|
|
// Angular should stop complaining about the NgModule.
|
|
const programAfter = env.tsLS.getProgram()!;
|
|
const moduleSfAfter = programAfter.getSourceFile(moduleFile)!;
|
|
const ngDiagsAfter = env.ngLS.compilerFactory.getOrCreate().getDiagnostics(moduleSfAfter);
|
|
expect(ngDiagsAfter.length).toBe(0);
|
|
});
|
|
});
|