diff --git a/packages/language-service/api.ts b/packages/language-service/api.ts index a78f3173e4..9424f5cde6 100644 --- a/packages/language-service/api.ts +++ b/packages/language-service/api.ts @@ -14,7 +14,7 @@ import * as ts from 'typescript'; -export interface NgLanguageServiceConfig { +export interface PluginConfig { /** * If true, return only Angular results. Otherwise, return Angular + TypeScript * results. @@ -25,6 +25,11 @@ export interface NgLanguageServiceConfig { * 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 = { diff --git a/packages/language-service/index.ts b/packages/language-service/index.ts index 07a78428c5..6b02ff5ca9 100644 --- a/packages/language-service/index.ts +++ b/packages/language-service/index.ts @@ -7,13 +7,13 @@ */ import * as ts from 'typescript/lib/tsserverlibrary'; -import {NgLanguageService, NgLanguageServiceConfig} from './api'; +import {NgLanguageService, PluginConfig} from './api'; export * from './api'; interface PluginModule extends ts.server.PluginModule { create(createInfo: ts.server.PluginCreateInfo): NgLanguageService; - onConfigurationChanged?(config: NgLanguageServiceConfig): void; + onConfigurationChanged?(config: PluginConfig): void; } const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => { @@ -21,7 +21,7 @@ const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => { return { create(info: ts.server.PluginCreateInfo): NgLanguageService { - const config: NgLanguageServiceConfig = info.config; + const config: PluginConfig = info.config; const bundleName = config.ivy ? 'ivy.js' : 'language-service.js'; plugin = require(`./bundles/${bundleName}`)(tsModule); return plugin.create(info); @@ -29,7 +29,7 @@ const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => { getExternalFiles(project: ts.server.Project): string[] { return plugin?.getExternalFiles?.(project) ?? []; }, - onConfigurationChanged(config: NgLanguageServiceConfig): void { + onConfigurationChanged(config: PluginConfig): void { plugin?.onConfigurationChanged?.(config); }, }; diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index e9defa5501..55131b02a4 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -29,6 +29,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; @@ -37,9 +45,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); @@ -340,7 +351,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); } }); @@ -354,7 +365,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 {}; } @@ -375,6 +387,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 d38c63406a..f5fc2b40ce 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 f3da97fc39..728deb23dc 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 6874be3bc0..65361603ac 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 ce6f75fc75..88599ccf1c 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 7b1c234469..821bb725b5 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 8838b5b229..5f2ca94203 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 87b89c4ff5..5d964b1db4 100644 --- a/packages/language-service/ivy/ts_plugin.ts +++ b/packages/language-service/ivy/ts_plugin.ts @@ -16,7 +16,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[] = [];