From de8f0fe5eea5b570e41f109406d722290e961629 Mon Sep 17 00:00:00 2001 From: Zach Arend Date: Thu, 3 Dec 2020 11:42:46 -0800 Subject: [PATCH] test(language-service): convert ivy diagnostics tests from legacy (#39957) This commit updates the tests for the diagnostics in the ivy language service to use the new in-memory test environment. PR Close #39957 --- .../ivy/test/diagnostic_spec.ts | 108 ++++++++++++++++++ .../ivy/test/legacy/daignostic_spec.ts | 41 +++++++ .../ivy/test/legacy/diagnostic_spec.ts | 75 ------------ .../ivy/test/references_spec.ts | 85 +++++--------- .../language-service/ivy/test/test_utils.ts | 41 +++++++ 5 files changed, 216 insertions(+), 134 deletions(-) create mode 100644 packages/language-service/ivy/test/diagnostic_spec.ts create mode 100644 packages/language-service/ivy/test/legacy/daignostic_spec.ts delete mode 100644 packages/language-service/ivy/test/legacy/diagnostic_spec.ts diff --git a/packages/language-service/ivy/test/diagnostic_spec.ts b/packages/language-service/ivy/test/diagnostic_spec.ts new file mode 100644 index 0000000000..0031673450 --- /dev/null +++ b/packages/language-service/ivy/test/diagnostic_spec.ts @@ -0,0 +1,108 @@ +/** + * @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} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env'; +import * as ts from 'typescript'; +import {createModuleWithDeclarations} from './test_utils'; + +describe('getSemanticDiagnostics', () => { + let env: LanguageServiceTestEnvironment; + + beforeEach(() => { + initMockFileSystem('Native'); + }); + + it('should not produce error for a minimal component defintion', () => { + const appFile = { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: '' + }) + export class AppComponent {} + ` + }; + env = createModuleWithDeclarations([appFile]); + + const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); + expect(diags.length).toEqual(0); + }); + + it('should report member does not exist', () => { + const appFile = { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: '{{nope}}' + }) + export class AppComponent {} + ` + }; + env = createModuleWithDeclarations([appFile]); + + const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.ts')); + expect(diags.length).toBe(1); + const {category, file, start, length, messageText} = diags[0]; + expect(category).toBe(ts.DiagnosticCategory.Error); + expect(file?.fileName).toBe('/app.ts'); + expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); + }); + + it('should process external template', () => { + const appFile = { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + templateUrl: './app.html' + }) + export class AppComponent {} + ` + }; + const templateFile = { + name: absoluteFrom('/app.html'), + contents: ` + Hello world! + ` + }; + + env = createModuleWithDeclarations([appFile], [templateFile]); + const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); + expect(diags).toEqual([]); + }); + + it('should report member does not exist in external template', () => { + const appFile = { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + templateUrl: './app.html' + }) + export class AppComponent {} + ` + }; + const templateFile = {name: absoluteFrom('/app.html'), contents: `{{nope}}`}; + + env = createModuleWithDeclarations([appFile], [templateFile]); + const diags = env.ngLS.getSemanticDiagnostics(absoluteFrom('/app.html')); + expect(diags.length).toBe(1); + const {category, file, start, length, messageText} = diags[0]; + expect(category).toBe(ts.DiagnosticCategory.Error); + expect(file?.fileName).toBe('/app.html'); + expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); + }); +}); diff --git a/packages/language-service/ivy/test/legacy/daignostic_spec.ts b/packages/language-service/ivy/test/legacy/daignostic_spec.ts new file mode 100644 index 0000000000..f3da97fc39 --- /dev/null +++ b/packages/language-service/ivy/test/legacy/daignostic_spec.ts @@ -0,0 +1,41 @@ +/** + * @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 {LanguageService} from '../../language_service'; + +import {MockService, setup, TEST_TEMPLATE} from './mock_host'; + +describe('getSemanticDiagnostics', () => { + let service: MockService; + let ngLS: LanguageService; + + beforeAll(() => { + const {project, service: _service, tsLS} = setup(); + service = _service; + ngLS = new LanguageService(project, tsLS); + }); + + beforeEach(() => { + service.reset(); + }); + + it('should retrieve external template from latest snapshot', () => { + // This test is to make sure we are reading from snapshot instead of disk + // if content from snapshot is newer. It also makes sure the internal cache + // of the resource loader is invalidated on content change. + service.overwrite(TEST_TEMPLATE, `{{ foo }}`); + const d1 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); + expect(d1.length).toBe(1); + expect(d1[0].messageText).toBe(`Property 'foo' does not exist on type 'TemplateReference'.`); + + service.overwrite(TEST_TEMPLATE, `{{ bar }}`); + const d2 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); + expect(d2.length).toBe(1); + expect(d2[0].messageText).toBe(`Property 'bar' does not exist on type 'TemplateReference'.`); + }); +}); diff --git a/packages/language-service/ivy/test/legacy/diagnostic_spec.ts b/packages/language-service/ivy/test/legacy/diagnostic_spec.ts deleted file mode 100644 index 1d8f6c4ef3..0000000000 --- a/packages/language-service/ivy/test/legacy/diagnostic_spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @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 * as ts from 'typescript/lib/tsserverlibrary'; - -import {LanguageService} from '../../language_service'; - -import {APP_COMPONENT, MockService, setup, TEST_TEMPLATE} from './mock_host'; - -describe('getSemanticDiagnostics', () => { - let service: MockService; - let ngLS: LanguageService; - - beforeAll(() => { - const {project, service: _service, tsLS} = setup(); - service = _service; - ngLS = new LanguageService(project, tsLS); - }); - - beforeEach(() => { - service.reset(); - }); - - it('should not produce error for AppComponent', () => { - const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT); - expect(diags).toEqual([]); - }); - - it('should report member does not exist', () => { - const {text} = service.overwriteInlineTemplate(APP_COMPONENT, '{{ nope }}'); - const diags = ngLS.getSemanticDiagnostics(APP_COMPONENT); - expect(diags.length).toBe(1); - const {category, file, start, length, messageText} = diags[0]; - expect(category).toBe(ts.DiagnosticCategory.Error); - expect(file?.fileName).toBe(APP_COMPONENT); - expect(text.substring(start!, start! + length!)).toBe('nope'); - expect(messageText).toBe(`Property 'nope' does not exist on type 'AppComponent'.`); - }); - - it('should process external template', () => { - const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); - expect(diags).toEqual([]); - }); - - it('should report member does not exist in external template', () => { - const {text} = service.overwrite(TEST_TEMPLATE, `{{ nope }}`); - const diags = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); - expect(diags.length).toBe(1); - const {category, file, start, length, messageText} = diags[0]; - expect(category).toBe(ts.DiagnosticCategory.Error); - expect(file?.fileName).toBe(TEST_TEMPLATE); - expect(text.substring(start!, start! + length!)).toBe('nope'); - expect(messageText).toBe(`Property 'nope' does not exist on type 'TemplateReference'.`); - }); - - it('should retrieve external template from latest snapshot', () => { - // This test is to make sure we are reading from snapshot instead of disk - // if content from snapshot is newer. It also makes sure the internal cache - // of the resource loader is invalidated on content change. - service.overwrite(TEST_TEMPLATE, `{{ foo }}`); - const d1 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); - expect(d1.length).toBe(1); - expect(d1[0].messageText).toBe(`Property 'foo' does not exist on type 'TemplateReference'.`); - - service.overwrite(TEST_TEMPLATE, `{{ bar }}`); - const d2 = ngLS.getSemanticDiagnostics(TEST_TEMPLATE); - expect(d2.length).toBe(1); - expect(d2[0].messageText).toBe(`Property 'bar' does not exist on type 'TemplateReference'.`); - }); -}); diff --git a/packages/language-service/ivy/test/references_spec.ts b/packages/language-service/ivy/test/references_spec.ts index 249f305cb9..b56e3ca8ff 100644 --- a/packages/language-service/ivy/test/references_spec.ts +++ b/packages/language-service/ivy/test/references_spec.ts @@ -11,7 +11,7 @@ import {initMockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file import * as ts from 'typescript/lib/tsserverlibrary'; import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; -import {getText} from './test_utils'; +import {createModuleWithDeclarations, getText} from './test_utils'; describe('find references', () => { let env: LanguageServiceTestEnvironment; @@ -30,7 +30,7 @@ describe('find references', () => { }`); const appFile = {name: _('/app.ts'), contents: text}; const templateFile = {name: _('/app.html'), contents: '{{myProp}}'}; - createModuleWithDeclarations([appFile], [templateFile]); + env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); @@ -46,7 +46,7 @@ describe('find references', () => { myP¦rop!: string; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); @@ -66,7 +66,7 @@ describe('find references', () => { }; const {text, cursor} = extractCursorInfo('{{myP¦rop}}'); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile], [templateFile]); + env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html', 'app.ts']); @@ -82,7 +82,7 @@ describe('find references', () => { setTitle(s: number) {} }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); @@ -100,7 +100,7 @@ describe('find references', () => { setTitle(s: string) {} }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); @@ -121,7 +121,7 @@ describe('find references', () => { const templateFileWithCursor = `
`; const {text, cursor} = extractCursorInfo(templateFileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile], [templateFile]); + env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); @@ -142,7 +142,7 @@ describe('find references', () => { name: _('/app.ts'), contents: text, }; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); @@ -162,7 +162,7 @@ describe('find references', () => { name: _('/app.ts'), contents: text, }; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; // 3 references: the type definition, the value assignment, and the read in the template expect(refs.length).toBe(3); @@ -191,7 +191,7 @@ describe('find references', () => { const templateFileWithCursor = `
`; const {text, cursor} = extractCursorInfo(templateFileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile], [templateFile]); + env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); @@ -209,7 +209,7 @@ describe('find references', () => { title = ''; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['myInput']); @@ -236,7 +236,7 @@ describe('find references', () => { }; const {text, cursor} = extractCursorInfo(templateWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile], [templateFile]); + env = createModuleWithDeclarations([appFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertTextSpans(refs, ['myTemplate']); @@ -274,7 +274,7 @@ describe('find references', () => { const templateWithCursor = '
{{ dirR¦ef }}'; const {text, cursor} = extractCursorInfo(templateWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile, dirFile], [templateFile]); + env = createModuleWithDeclarations([appFile, dirFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.html']); @@ -285,7 +285,7 @@ describe('find references', () => { const fileWithCursor = '
{{ dirRef.dirV¦alue }}'; const {text, cursor} = extractCursorInfo(fileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile, dirFile], [templateFile]); + env = createModuleWithDeclarations([appFile, dirFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['dir.ts', 'app.html']); @@ -296,7 +296,7 @@ describe('find references', () => { const fileWithCursor = '
{{ dirRef?.dirV¦alue }}'; const {text, cursor} = extractCursorInfo(fileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile, dirFile], [templateFile]); + env = createModuleWithDeclarations([appFile, dirFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['dir.ts', 'app.html']); @@ -307,7 +307,7 @@ describe('find references', () => { const fileWithCursor = '
{{ dirRef?.doSometh¦ing() }}'; const {text, cursor} = extractCursorInfo(fileWithCursor); const templateFile = {name: _('/app.html'), contents: text}; - createModuleWithDeclarations([appFile, dirFile], [templateFile]); + env = createModuleWithDeclarations([appFile, dirFile], [templateFile]); const refs = getReferencesAtPosition(_('/app.html'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['dir.ts', 'app.html']); @@ -326,7 +326,7 @@ describe('find references', () => { heroes: string[] = []; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); @@ -347,7 +347,7 @@ describe('find references', () => { heroes: string[] = []; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); @@ -408,7 +408,7 @@ describe('find references', () => { heroes: Array<{name: string}> = []; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile]); + env = createModuleWithDeclarations([appFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); @@ -444,7 +444,7 @@ describe('find references', () => { `; const {text, cursor} = extractCursorInfo(appContentsWithCursor); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile, prefixPipeFile]); + env = createModuleWithDeclarations([appFile, prefixPipeFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(5); assertFileNames(refs, ['index.d.ts', 'prefix-pipe.ts', 'app.ts']); @@ -463,7 +463,7 @@ describe('find references', () => { `; const {text, cursor} = extractCursorInfo(appContentsWithCursor); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile, prefixPipeFile]); + env = createModuleWithDeclarations([appFile, prefixPipeFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toBe(2); assertFileNames(refs, ['app.ts']); @@ -490,7 +490,7 @@ describe('find references', () => { title = 'title'; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile, stringModelTestFile]); + env = createModuleWithDeclarations([appFile, stringModelTestFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toEqual(2); assertFileNames(refs, ['string-model.ts', 'app.ts']); @@ -507,7 +507,7 @@ describe('find references', () => { title = 'title'; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile, stringModelTestFile]); + env = createModuleWithDeclarations([appFile, stringModelTestFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toEqual(2); assertFileNames(refs, ['string-model.ts', 'app.ts']); @@ -534,7 +534,7 @@ describe('find references', () => { title = 'title'; }`, }; - createModuleWithDeclarations([appFile, stringModelTestFile]); + env = createModuleWithDeclarations([appFile, stringModelTestFile]); const refs = getReferencesAtPosition(_('/string-model.ts'), cursor)!; expect(refs.length).toEqual(2); assertFileNames(refs, ['app.ts', 'string-model.ts']); @@ -576,7 +576,7 @@ describe('find references', () => { title = 'title'; }`, }; - createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]); + env = createModuleWithDeclarations([appFile, stringModelTestFile, otherDirFile]); const refs = getReferencesAtPosition(_('/other-dir.ts'), cursor)!; expect(refs.length).toEqual(3); assertFileNames(refs, ['app.ts', 'string-model.ts', 'other-dir.ts']); @@ -593,7 +593,7 @@ describe('find references', () => { title = 'title'; }`); const appFile = {name: _('/app.ts'), contents: text}; - createModuleWithDeclarations([appFile, stringModelTestFile]); + env = createModuleWithDeclarations([appFile, stringModelTestFile]); const refs = getReferencesAtPosition(_('/app.ts'), cursor)!; expect(refs.length).toEqual(2); assertFileNames(refs, ['string-model.ts', 'app.ts']); @@ -703,39 +703,6 @@ describe('find references', () => { entry.originalContextSpan ? getText(fileContents, entry.originalContextSpan) : undefined, }; } - - function getFirstClassDeclaration(declaration: string) { - const matches = declaration.match(/(?:export class )(\w+)(?:\s|\{)/); - if (matches === null || matches.length !== 2) { - throw new Error(`Did not find exactly one exported class in: ${declaration}`); - } - return matches[1].trim(); - } - - function createModuleWithDeclarations( - filesWithClassDeclarations: TestFile[], externalResourceFiles: TestFile[] = []): void { - const externalClasses = - filesWithClassDeclarations.map(file => getFirstClassDeclaration(file.contents)); - const externalImports = filesWithClassDeclarations.map(file => { - const className = getFirstClassDeclaration(file.contents); - const fileName = last(file.name.split('/')).replace('.ts', ''); - return `import {${className}} from './${fileName}';`; - }); - const contents = ` - import {NgModule} from '@angular/core'; - import {CommonModule} from '@angular/common'; - ${externalImports.join('\n')} - - @NgModule({ - declarations: [${externalClasses.join(',')}], - imports: [CommonModule], - }) - export class AppModule {} - `; - const moduleFile = {name: _('/app-module.ts'), contents, isRoot: true}; - env = LanguageServiceTestEnvironment.setup( - [moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]); - } }); function assertFileNames(refs: Array<{fileName: string}>, expectedFileNames: string[]) { diff --git a/packages/language-service/ivy/test/test_utils.ts b/packages/language-service/ivy/test/test_utils.ts index 9a96c2bad3..2d7253f91b 100644 --- a/packages/language-service/ivy/test/test_utils.ts +++ b/packages/language-service/ivy/test/test_utils.ts @@ -6,7 +6,48 @@ * found in the LICENSE file at https://angular.io/license */ +import {absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {LanguageServiceTestEnvironment} from '@angular/language-service/ivy/test/env'; export function getText(contents: string, textSpan: ts.TextSpan) { return contents.substr(textSpan.start, textSpan.length); } + +function last(array: T[]): T { + return array[array.length - 1]; +} + +function getFirstClassDeclaration(declaration: string) { + const matches = declaration.match(/(?:export class )(\w+)(?:\s|\{)/); + if (matches === null || matches.length !== 2) { + throw new Error(`Did not find exactly one exported class in: ${declaration}`); + } + return matches[1].trim(); +} + +export function createModuleWithDeclarations( + filesWithClassDeclarations: TestFile[], + externalResourceFiles: TestFile[] = []): LanguageServiceTestEnvironment { + const externalClasses = + filesWithClassDeclarations.map(file => getFirstClassDeclaration(file.contents)); + const externalImports = filesWithClassDeclarations.map(file => { + const className = getFirstClassDeclaration(file.contents); + const fileName = last(file.name.split('/')).replace('.ts', ''); + return `import {${className}} from './${fileName}';`; + }); + const contents = ` + import {NgModule} from '@angular/core'; + import {CommonModule} from '@angular/common'; + ${externalImports.join('\n')} + + @NgModule({ + declarations: [${externalClasses.join(',')}], + imports: [CommonModule], + }) + export class AppModule {} + `; + const moduleFile = {name: _('/app-module.ts'), contents, isRoot: true}; + return LanguageServiceTestEnvironment.setup( + [moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles]); +}