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:
Keen Yee Liau 2021-01-13 14:52:29 -08:00 committed by Jessica Janiuk
parent c9fe455fa2
commit ecae75f477
10 changed files with 112 additions and 22 deletions

View File

@ -35,5 +35,6 @@ export declare enum ErrorCode {
DUPLICATE_VARIABLE_DECLARATION = 8006,
INLINE_TCB_REQUIRED = 8900,
INLINE_TYPE_CTOR_REQUIRED = 8901,
INJECTABLE_DUPLICATE_PROV = 9001
INJECTABLE_DUPLICATE_PROV = 9001,
SUGGEST_STRICT_TEMPLATES = 10001
}

View File

@ -127,9 +127,9 @@ export interface StrictTemplateOptions {
/**
* 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;

View File

@ -906,7 +906,7 @@ function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
/**
* 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.
*/
function verifyCompatibleTypeCheckOptions(options: NgCompilerOptions): ts.Diagnostic|null {

View File

@ -159,6 +159,16 @@ export enum ErrorCode {
* An injectable already has a `ɵprov` property.
*/
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,
}
/**

View File

@ -10,6 +10,7 @@ ts_library(
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/core",
"//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/imports",
"//packages/compiler-cli/src/ngtsc/incremental",

View File

@ -9,6 +9,7 @@
import {AbsoluteSourceSpan, AST, ParseSourceSpan, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
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 {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
@ -50,7 +51,8 @@ export class LanguageService {
private readonly adapter: LanguageServiceAdapter;
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.options = parseNgCompilerOptions(project, this.parseConfigHost);
logCompilerOptions(project, this.options);
@ -268,6 +270,34 @@ export class LanguageService {
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) {
// TODO: Check the case when the project is disposed. An InferredProject
// could be disposed when a tsconfig.json is added to the workspace,

View File

@ -6,6 +6,8 @@ ts_library(
srcs = glob(["*.ts"]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/language-service/ivy",
"@npm//typescript",
],

View File

@ -6,6 +6,7 @@
* 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 {LanguageService} from '../../language_service';
@ -31,15 +32,6 @@ describe('language service adapter', () => {
});
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', () => {
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
enableIvy: true, // default for ivy is true
@ -55,11 +47,11 @@ describe('language service adapter', () => {
strictInjectionParameters: true,
}));
configFileFs.overwriteConfigFile(TSCONFIG, `{
"angularCompilerOptions": {
"strictTemplates": false
configFileFs.overwriteConfigFile(TSCONFIG, {
angularCompilerOptions: {
strictTemplates: false,
}
}`);
});
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
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', () => {
beforeEach(() => {

View File

@ -6,6 +6,7 @@
* 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 * as ts from 'typescript/lib/tsserverlibrary';
@ -68,11 +69,11 @@ export class MockConfigFileFs implements
private configOverwrites = new Map<string, string>();
private configFileWatchers = new Map<string, MockWatcher>();
overwriteConfigFile(configFile: string, contents: string) {
overwriteConfigFile(configFile: string, contents: {angularCompilerOptions?: NgCompilerOptions}) {
if (!configFile.endsWith('.json')) {
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();
}
@ -98,8 +99,11 @@ export class MockConfigFileFs implements
}
clear() {
for (const [fileName, watcher] of this.configFileWatchers) {
this.configOverwrites.delete(fileName);
watcher.changed();
}
this.configOverwrites.clear();
this.configFileWatchers.clear();
}
}

View File

@ -119,6 +119,17 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
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 {
return ngLS.getTcb(fileName, position);
@ -137,6 +148,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
getCompletionEntryDetails,
getCompletionEntrySymbol,
getTcb,
getCompilerOptionsDiagnostics,
};
}