feat(language-service): Add diagnostics to suggest turning on strict mode (#40423)
This PR adds a way for the language server to retrieve compiler options diagnostics via `languageService.getCompilerOptionsDiagnostics()`. This will be used by the language server to show a prompt in the editor if users don't have `strict` or `fullTemplateTypeCheck` turned on. Ref https://github.com/angular/vscode-ng-language-service/issues/1053 PR Close #40423
This commit is contained in:
parent
c9fe455fa2
commit
ecae75f477
|
@ -35,5 +35,6 @@ export declare enum ErrorCode {
|
||||||
DUPLICATE_VARIABLE_DECLARATION = 8006,
|
DUPLICATE_VARIABLE_DECLARATION = 8006,
|
||||||
INLINE_TCB_REQUIRED = 8900,
|
INLINE_TCB_REQUIRED = 8900,
|
||||||
INLINE_TYPE_CTOR_REQUIRED = 8901,
|
INLINE_TYPE_CTOR_REQUIRED = 8901,
|
||||||
INJECTABLE_DUPLICATE_PROV = 9001
|
INJECTABLE_DUPLICATE_PROV = 9001,
|
||||||
|
SUGGEST_STRICT_TEMPLATES = 10001
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,9 +127,9 @@ export interface StrictTemplateOptions {
|
||||||
/**
|
/**
|
||||||
* If `true`, implies all template strictness flags below (unless individually disabled).
|
* If `true`, implies all template strictness flags below (unless individually disabled).
|
||||||
*
|
*
|
||||||
* Has no effect unless `fullTemplateTypeCheck` is also enabled.
|
* This flag is a superset of `fullTemplateTypeCheck`.
|
||||||
*
|
*
|
||||||
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
|
* Defaults to `false`, even if "fullTemplateTypeCheck" is `true`.
|
||||||
*/
|
*/
|
||||||
strictTemplates?: boolean;
|
strictTemplates?: boolean;
|
||||||
|
|
||||||
|
|
|
@ -906,7 +906,7 @@ function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since "strictTemplates" is a true superset of type checking capabilities compared to
|
* Since "strictTemplates" is a true superset of type checking capabilities compared to
|
||||||
* "strictTemplateTypeCheck", it is required that the latter is not explicitly disabled if the
|
* "fullTemplateTypeCheck", it is required that the latter is not explicitly disabled if the
|
||||||
* former is enabled.
|
* former is enabled.
|
||||||
*/
|
*/
|
||||||
function verifyCompatibleTypeCheckOptions(options: NgCompilerOptions): ts.Diagnostic|null {
|
function verifyCompatibleTypeCheckOptions(options: NgCompilerOptions): ts.Diagnostic|null {
|
||||||
|
|
|
@ -159,6 +159,16 @@ export enum ErrorCode {
|
||||||
* An injectable already has a `ɵprov` property.
|
* An injectable already has a `ɵprov` property.
|
||||||
*/
|
*/
|
||||||
INJECTABLE_DUPLICATE_PROV = 9001,
|
INJECTABLE_DUPLICATE_PROV = 9001,
|
||||||
|
|
||||||
|
// 10XXX error codes are reserved for diagnostics with category
|
||||||
|
// `ts.DiagnosticCategory.Suggestion`. These diagnostics are generated by
|
||||||
|
// language service.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggest users to enable `strictTemplates` to make use of full capabilities
|
||||||
|
* provided by Angular language service.
|
||||||
|
*/
|
||||||
|
SUGGEST_STRICT_TEMPLATES = 10001,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,7 @@ ts_library(
|
||||||
"//packages/compiler-cli",
|
"//packages/compiler-cli",
|
||||||
"//packages/compiler-cli/src/ngtsc/core",
|
"//packages/compiler-cli/src/ngtsc/core",
|
||||||
"//packages/compiler-cli/src/ngtsc/core:api",
|
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||||
"//packages/compiler-cli/src/ngtsc/imports",
|
"//packages/compiler-cli/src/ngtsc/imports",
|
||||||
"//packages/compiler-cli/src/ngtsc/incremental",
|
"//packages/compiler-cli/src/ngtsc/incremental",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import {AbsoluteSourceSpan, AST, ParseSourceSpan, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
|
import {AbsoluteSourceSpan, AST, ParseSourceSpan, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
|
||||||
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
|
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
|
||||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
|
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
|
||||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
||||||
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
|
@ -50,7 +51,8 @@ export class LanguageService {
|
||||||
private readonly adapter: LanguageServiceAdapter;
|
private readonly adapter: LanguageServiceAdapter;
|
||||||
private readonly parseConfigHost: LSParseConfigHost;
|
private readonly parseConfigHost: LSParseConfigHost;
|
||||||
|
|
||||||
constructor(project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
constructor(
|
||||||
|
private readonly project: ts.server.Project, private readonly tsLS: ts.LanguageService) {
|
||||||
this.parseConfigHost = new LSParseConfigHost(project.projectService.host);
|
this.parseConfigHost = new LSParseConfigHost(project.projectService.host);
|
||||||
this.options = parseNgCompilerOptions(project, this.parseConfigHost);
|
this.options = parseNgCompilerOptions(project, this.parseConfigHost);
|
||||||
logCompilerOptions(project, this.options);
|
logCompilerOptions(project, this.options);
|
||||||
|
@ -268,6 +270,34 @@ export class LanguageService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
|
||||||
|
const project = this.project;
|
||||||
|
if (!(project instanceof ts.server.ConfiguredProject)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagnostics: ts.Diagnostic[] = [];
|
||||||
|
const configSourceFile = ts.readJsonConfigFile(
|
||||||
|
project.getConfigFilePath(), (path: string) => project.readFile(path));
|
||||||
|
|
||||||
|
if (!this.options.strictTemplates && !this.options.fullTemplateTypeCheck) {
|
||||||
|
diagnostics.push({
|
||||||
|
messageText: 'Some language features are not available. ' +
|
||||||
|
'To access all features, enable `strictTemplates` in `angularCompilerOptions`.',
|
||||||
|
category: ts.DiagnosticCategory.Suggestion,
|
||||||
|
code: ngErrorCode(ErrorCode.SUGGEST_STRICT_TEMPLATES),
|
||||||
|
file: configSourceFile,
|
||||||
|
start: undefined,
|
||||||
|
length: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiler = this.compilerFactory.getOrCreate();
|
||||||
|
diagnostics.push(...compiler.getOptionDiagnostics());
|
||||||
|
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
private watchConfigFile(project: ts.server.Project) {
|
private watchConfigFile(project: ts.server.Project) {
|
||||||
// TODO: Check the case when the project is disposed. An InferredProject
|
// TODO: Check the case when the project is disposed. An InferredProject
|
||||||
// could be disposed when a tsconfig.json is added to the workspace,
|
// could be disposed when a tsconfig.json is added to the workspace,
|
||||||
|
|
|
@ -6,6 +6,8 @@ ts_library(
|
||||||
srcs = glob(["*.ts"]),
|
srcs = glob(["*.ts"]),
|
||||||
deps = [
|
deps = [
|
||||||
"//packages/compiler",
|
"//packages/compiler",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/core:api",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
"//packages/language-service/ivy",
|
"//packages/language-service/ivy",
|
||||||
"@npm//typescript",
|
"@npm//typescript",
|
||||||
],
|
],
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
import {LanguageService} from '../../language_service';
|
import {LanguageService} from '../../language_service';
|
||||||
|
@ -31,15 +32,6 @@ describe('language service adapter', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parse compiler options', () => {
|
describe('parse compiler options', () => {
|
||||||
beforeEach(() => {
|
|
||||||
// Need to reset project on each test to reinitialize file watchers.
|
|
||||||
const {project: _project, tsLS, service: _service, configFileFs: _configFileFs} = setup();
|
|
||||||
project = _project;
|
|
||||||
service = _service;
|
|
||||||
configFileFs = _configFileFs;
|
|
||||||
ngLS = new LanguageService(project, tsLS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize with angularCompilerOptions from tsconfig.json', () => {
|
it('should initialize with angularCompilerOptions from tsconfig.json', () => {
|
||||||
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
|
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
|
||||||
enableIvy: true, // default for ivy is true
|
enableIvy: true, // default for ivy is true
|
||||||
|
@ -55,11 +47,11 @@ describe('language service adapter', () => {
|
||||||
strictInjectionParameters: true,
|
strictInjectionParameters: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
configFileFs.overwriteConfigFile(TSCONFIG, `{
|
configFileFs.overwriteConfigFile(TSCONFIG, {
|
||||||
"angularCompilerOptions": {
|
angularCompilerOptions: {
|
||||||
"strictTemplates": false
|
strictTemplates: false,
|
||||||
}
|
}
|
||||||
}`);
|
});
|
||||||
|
|
||||||
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
|
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
|
||||||
strictTemplates: false,
|
strictTemplates: false,
|
||||||
|
@ -67,6 +59,44 @@ describe('language service adapter', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('compiler options diagnostics', () => {
|
||||||
|
it('suggests turning on strict flag', () => {
|
||||||
|
configFileFs.overwriteConfigFile(TSCONFIG, {
|
||||||
|
angularCompilerOptions: {},
|
||||||
|
});
|
||||||
|
const diags = ngLS.getCompilerOptionsDiagnostics();
|
||||||
|
const diag = diags.find(isSuggestStrictTemplatesDiag);
|
||||||
|
expect(diag).toBeDefined();
|
||||||
|
expect(diag!.category).toBe(ts.DiagnosticCategory.Suggestion);
|
||||||
|
expect(diag!.file?.getSourceFile().fileName).toBe(TSCONFIG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not suggest turning on strict mode is strictTemplates flag is on', () => {
|
||||||
|
configFileFs.overwriteConfigFile(TSCONFIG, {
|
||||||
|
angularCompilerOptions: {
|
||||||
|
strictTemplates: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const diags = ngLS.getCompilerOptionsDiagnostics();
|
||||||
|
const diag = diags.find(isSuggestStrictTemplatesDiag);
|
||||||
|
expect(diag).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not suggest turning on strict mode is fullTemplateTypeCheck flag is on', () => {
|
||||||
|
configFileFs.overwriteConfigFile(TSCONFIG, {
|
||||||
|
angularCompilerOptions: {
|
||||||
|
fullTemplateTypeCheck: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const diags = ngLS.getCompilerOptionsDiagnostics();
|
||||||
|
const diag = diags.find(isSuggestStrictTemplatesDiag);
|
||||||
|
expect(diag).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
function isSuggestStrictTemplatesDiag(diag: ts.Diagnostic) {
|
||||||
|
return diag.code === ngErrorCode(ErrorCode.SUGGEST_STRICT_TEMPLATES);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe('last known program', () => {
|
describe('last known program', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||||
|
|
||||||
|
@ -68,11 +69,11 @@ export class MockConfigFileFs implements
|
||||||
private configOverwrites = new Map<string, string>();
|
private configOverwrites = new Map<string, string>();
|
||||||
private configFileWatchers = new Map<string, MockWatcher>();
|
private configFileWatchers = new Map<string, MockWatcher>();
|
||||||
|
|
||||||
overwriteConfigFile(configFile: string, contents: string) {
|
overwriteConfigFile(configFile: string, contents: {angularCompilerOptions?: NgCompilerOptions}) {
|
||||||
if (!configFile.endsWith('.json')) {
|
if (!configFile.endsWith('.json')) {
|
||||||
throw new Error(`${configFile} is not a configuration file.`);
|
throw new Error(`${configFile} is not a configuration file.`);
|
||||||
}
|
}
|
||||||
this.configOverwrites.set(configFile, contents);
|
this.configOverwrites.set(configFile, JSON.stringify(contents));
|
||||||
this.configFileWatchers.get(configFile)?.changed();
|
this.configFileWatchers.get(configFile)?.changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,8 +99,11 @@ export class MockConfigFileFs implements
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
for (const [fileName, watcher] of this.configFileWatchers) {
|
||||||
|
this.configOverwrites.delete(fileName);
|
||||||
|
watcher.changed();
|
||||||
|
}
|
||||||
this.configOverwrites.clear();
|
this.configOverwrites.clear();
|
||||||
this.configFileWatchers.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,17 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
|
||||||
ngLS.getCompletionEntrySymbol(fileName, position, name);
|
ngLS.getCompletionEntrySymbol(fileName, position, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets global diagnostics related to the program configuration and compiler options.
|
||||||
|
*/
|
||||||
|
function getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
|
||||||
|
const diagnostics: ts.Diagnostic[] = [];
|
||||||
|
if (!angularOnly) {
|
||||||
|
diagnostics.push(...tsLS.getCompilerOptionsDiagnostics());
|
||||||
|
}
|
||||||
|
diagnostics.push(...ngLS.getCompilerOptionsDiagnostics());
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
function getTcb(fileName: string, position: number): GetTcbResponse {
|
function getTcb(fileName: string, position: number): GetTcbResponse {
|
||||||
return ngLS.getTcb(fileName, position);
|
return ngLS.getTcb(fileName, position);
|
||||||
|
@ -137,6 +148,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
|
||||||
getCompletionEntryDetails,
|
getCompletionEntryDetails,
|
||||||
getCompletionEntrySymbol,
|
getCompletionEntrySymbol,
|
||||||
getTcb,
|
getTcb,
|
||||||
|
getCompilerOptionsDiagnostics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue