diff --git a/packages/language-service/api.ts b/packages/language-service/api.ts index 05cd562df8d..f52a57e1ca1 100644 --- a/packages/language-service/api.ts +++ b/packages/language-service/api.ts @@ -14,6 +14,24 @@ import * as ts from 'typescript'; +export interface PluginConfig { + /** + * If true, return only Angular results. Otherwise, return Angular + TypeScript + * results. + */ + angularOnly: boolean; + /** + * If true, return factory function for Ivy LS during plugin initialization. + * Otherwise return factory function for View Engine LS. + */ + ivy: boolean; + /** + * If true, enable `strictTemplates` in Angular compiler options regardless + * of its value in tsconfig.json. + */ + forceStrictTemplates?: true; +} + export type GetTcbResponse = { /** * The filename of the SourceFile this typecheck block belongs to. diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 5147df8aba6..ef3180a0350 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -27,6 +27,14 @@ import {getTargetAtPosition, TargetContext, TargetNodeKind} from './template_tar import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils'; +interface LanguageServiceConfig { + /** + * If true, enable `strictTemplates` in Angular compiler options regardless + * of its value in tsconfig.json. + */ + forceStrictTemplates?: true; +} + export class LanguageService { private options: CompilerOptions; readonly compilerFactory: CompilerFactory; @@ -35,9 +43,12 @@ export class LanguageService { private readonly parseConfigHost: LSParseConfigHost; constructor( - private readonly project: ts.server.Project, private readonly tsLS: ts.LanguageService) { + private readonly project: ts.server.Project, + private readonly tsLS: ts.LanguageService, + private readonly config: LanguageServiceConfig, + ) { this.parseConfigHost = new LSParseConfigHost(project.projectService.host); - this.options = parseNgCompilerOptions(project, this.parseConfigHost); + this.options = parseNgCompilerOptions(project, this.parseConfigHost, config); logCompilerOptions(project, this.options); this.strategy = createTypeCheckingProgramStrategy(project); this.adapter = new LanguageServiceAdapter(project); @@ -315,7 +326,7 @@ export class LanguageService { project.getConfigFilePath(), (fileName: string, eventKind: ts.FileWatcherEventKind) => { project.log(`Config file changed: ${fileName}`); if (eventKind === ts.FileWatcherEventKind.Changed) { - this.options = parseNgCompilerOptions(project, this.parseConfigHost); + this.options = parseNgCompilerOptions(project, this.parseConfigHost, this.config); logCompilerOptions(project, this.options); } }); @@ -329,7 +340,8 @@ function logCompilerOptions(project: ts.server.Project, options: CompilerOptions } function parseNgCompilerOptions( - project: ts.server.Project, host: ConfigurationHost): CompilerOptions { + project: ts.server.Project, host: ConfigurationHost, + config: LanguageServiceConfig): CompilerOptions { if (!(project instanceof ts.server.ConfiguredProject)) { return {}; } @@ -350,6 +362,12 @@ function parseNgCompilerOptions( // and only the real component declaration is used. options.compileNonExportedClasses = false; + // If `forceStrictTemplates` is true, always enable `strictTemplates` + // regardless of its value in tsconfig.json. + if (config.forceStrictTemplates === true) { + options.strictTemplates = true; + } + return options; } diff --git a/packages/language-service/ivy/test/legacy/definitions_spec.ts b/packages/language-service/ivy/test/legacy/definitions_spec.ts index d38c63406a6..f5fc2b40cec 100644 --- a/packages/language-service/ivy/test/legacy/definitions_spec.ts +++ b/packages/language-service/ivy/test/legacy/definitions_spec.ts @@ -18,7 +18,7 @@ describe('definitions', () => { beforeAll(() => { const {project, service: _service, tsLS} = setup(); service = _service; - ngLS = new LanguageService(project, tsLS); + ngLS = new LanguageService(project, tsLS, {}); }); beforeEach(() => { diff --git a/packages/language-service/ivy/test/legacy/daignostic_spec.ts b/packages/language-service/ivy/test/legacy/diagnostic_spec.ts similarity index 96% rename from packages/language-service/ivy/test/legacy/daignostic_spec.ts rename to packages/language-service/ivy/test/legacy/diagnostic_spec.ts index f3da97fc390..728deb23dc9 100644 --- a/packages/language-service/ivy/test/legacy/daignostic_spec.ts +++ b/packages/language-service/ivy/test/legacy/diagnostic_spec.ts @@ -17,7 +17,7 @@ describe('getSemanticDiagnostics', () => { beforeAll(() => { const {project, service: _service, tsLS} = setup(); service = _service; - ngLS = new LanguageService(project, tsLS); + ngLS = new LanguageService(project, tsLS, {}); }); beforeEach(() => { diff --git a/packages/language-service/ivy/test/legacy/language_service_spec.ts b/packages/language-service/ivy/test/legacy/language_service_spec.ts index 6874be3bc04..65361603ac9 100644 --- a/packages/language-service/ivy/test/legacy/language_service_spec.ts +++ b/packages/language-service/ivy/test/legacy/language_service_spec.ts @@ -23,7 +23,7 @@ describe('language service adapter', () => { const {project: _project, tsLS, service: _service, configFileFs: _configFileFs} = setup(); project = _project; service = _service; - ngLS = new LanguageService(project, tsLS); + ngLS = new LanguageService(project, tsLS, {}); configFileFs = _configFileFs; }); @@ -57,6 +57,33 @@ describe('language service adapter', () => { strictTemplates: false, })); }); + + it('should always enable strictTemplates if forceStrictTemplates is true', () => { + const {project, tsLS, configFileFs} = setup(); + const ngLS = new LanguageService(project, tsLS, { + forceStrictTemplates: true, + }); + + // First make sure the default for strictTemplates is true + expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({ + enableIvy: true, // default for ivy is true + strictTemplates: true, + strictInjectionParameters: true, + })); + + // Change strictTemplates to false + configFileFs.overwriteConfigFile(TSCONFIG, { + angularCompilerOptions: { + strictTemplates: false, + } + }); + + // Make sure strictTemplates is still true because forceStrictTemplates + // is enabled. + expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({ + strictTemplates: true, + })); + }); }); describe('compiler options diagnostics', () => { diff --git a/packages/language-service/ivy/test/legacy/ts_plugin_spec.ts b/packages/language-service/ivy/test/legacy/ts_plugin_spec.ts index ce6f75fc752..88599ccf1ce 100644 --- a/packages/language-service/ivy/test/legacy/ts_plugin_spec.ts +++ b/packages/language-service/ivy/test/legacy/ts_plugin_spec.ts @@ -19,7 +19,7 @@ describe('getExternalFiles()', () => { // a global analysis expect(externalFiles).toEqual([]); // Trigger global analysis - const ngLS = new LanguageService(project, tsLS); + const ngLS = new LanguageService(project, tsLS, {}); ngLS.getSemanticDiagnostics(APP_COMPONENT); // Now that global analysis is run, we should have all the typecheck files externalFiles = getExternalFiles(project); diff --git a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts index 7b1c234469c..821bb725b59 100644 --- a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts +++ b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts @@ -18,7 +18,7 @@ describe('type definitions', () => { beforeAll(() => { const {project, service: _service, tsLS} = setup(); service = _service; - ngLS = new LanguageService(project, tsLS); + ngLS = new LanguageService(project, tsLS, {}); }); const possibleArrayDefFiles = new Set([ diff --git a/packages/language-service/ivy/testing/src/project.ts b/packages/language-service/ivy/testing/src/project.ts index 8838b5b2291..5f2ca942039 100644 --- a/packages/language-service/ivy/testing/src/project.ts +++ b/packages/language-service/ivy/testing/src/project.ts @@ -92,7 +92,7 @@ export class Project { // The following operation forces a ts.Program to be created. this.tsLS = tsProject.getLanguageService(); - this.ngLS = new LanguageService(tsProject, this.tsLS); + this.ngLS = new LanguageService(tsProject, this.tsLS, {}); } openFile(projectFileName: string): OpenBuffer { @@ -188,4 +188,4 @@ function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration { } } throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`); -} \ No newline at end of file +} diff --git a/packages/language-service/ivy/ts_plugin.ts b/packages/language-service/ivy/ts_plugin.ts index 6002a511c16..03d9430768e 100644 --- a/packages/language-service/ivy/ts_plugin.ts +++ b/packages/language-service/ivy/ts_plugin.ts @@ -14,7 +14,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService { const {project, languageService: tsLS, config} = info; const angularOnly = config?.angularOnly === true; - const ngLS = new LanguageService(project, tsLS); + const ngLS = new LanguageService(project, tsLS, config); function getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = [];