Skip to content

Commit 598b72b

Browse files
ivanwonderAndrewKushnir
authored andcommitted
feat(language-service): support fix the component missing member (#46764)
The diagnostic of the component missing member comes from the ts service, so the all code fixes for it are delegated to the ts service. The code fixes are placed in the LS package because only LS can benefit from it now, and The LS knows how to provide code fixes by the diagnostic and NgCompiler. The class `CodeFixes` is useful to extend the code fixes if LS needs to provide more code fixes for the template in the future. The ts service uses the same way to provide code fixes. https://github.com/microsoft/TypeScript/blob/162224763681465b417274383317ca9a0a573835/src/services/codeFixProvider.ts#L22 Fixes angular/vscode-ng-language-service#1610 PR Close #46764
1 parent d8cf78b commit 598b72b

File tree

12 files changed

+801
-32
lines changed

12 files changed

+801
-32
lines changed

packages/compiler-cli/src/ngtsc/perf/src/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ export enum PerfPhase {
153153
* Tracks the number of `PerfPhase`s, and must appear at the end of the list.
154154
*/
155155
LAST,
156+
157+
/**
158+
* Time spent by the Angular Language Service calculating code fixes.
159+
*/
160+
LsCodeFixes,
161+
162+
/**
163+
* Time spent by the Angular Language Service to fix all detected same type errors.
164+
*/
165+
LsCodeFixesAll,
156166
}
157167

158168
/**

packages/language-service/src/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ package(default_visibility = ["//packages/language-service:__subpackages__"])
44

55
ts_library(
66
name = "src",
7-
srcs = glob(["*.ts"]),
7+
srcs = glob([
8+
"*.ts",
9+
"**/*.ts",
10+
]),
811
deps = [
912
"//packages/compiler",
1013
"//packages/compiler-cli",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {missingMemberMeta} from './fix_missing_member';
10+
import {CodeActionMeta} from './utils';
11+
12+
export const ALL_CODE_FIXES_METAS: CodeActionMeta[] = [missingMemberMeta];
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
10+
import * as tss from 'typescript/lib/tsserverlibrary';
11+
12+
import {TemplateInfo} from '../utils';
13+
14+
import {CodeActionMeta, FixIdForCodeFixesAll, isFixAllAvailable} from './utils';
15+
16+
export class CodeFixes {
17+
private errorCodeToFixes: Map<number, CodeActionMeta[]> = new Map();
18+
private fixIdToRegistration = new Map<FixIdForCodeFixesAll, CodeActionMeta>();
19+
20+
constructor(
21+
private readonly tsLS: tss.LanguageService, readonly codeActionMetas: CodeActionMeta[]) {
22+
for (const meta of codeActionMetas) {
23+
for (const err of meta.errorCodes) {
24+
let errMeta = this.errorCodeToFixes.get(err);
25+
if (errMeta === undefined) {
26+
this.errorCodeToFixes.set(err, errMeta = []);
27+
}
28+
errMeta.push(meta);
29+
}
30+
for (const fixId of meta.fixIds) {
31+
if (this.fixIdToRegistration.has(fixId)) {
32+
// https://github.com/microsoft/TypeScript/blob/28dc248e5c500c7be9a8c3a7341d303e026b023f/src/services/codeFixProvider.ts#L28
33+
// In ts services, only one meta can be registered for a fixId.
34+
continue;
35+
}
36+
this.fixIdToRegistration.set(fixId, meta);
37+
}
38+
}
39+
}
40+
41+
/**
42+
* When the user moves the cursor or hovers on a diagnostics, this function will be invoked by LS,
43+
* and collect all the responses from the `codeActionMetas` which could handle the `errorCodes`.
44+
*/
45+
getCodeFixesAtPosition(
46+
templateInfo: TemplateInfo, compiler: NgCompiler, start: number, end: number,
47+
errorCodes: readonly number[], diagnostics: tss.Diagnostic[],
48+
formatOptions: tss.FormatCodeSettings,
49+
preferences: tss.UserPreferences): readonly tss.CodeFixAction[] {
50+
const codeActions: tss.CodeFixAction[] = [];
51+
for (const code of errorCodes) {
52+
const metas = this.errorCodeToFixes.get(code);
53+
if (metas === undefined) {
54+
continue;
55+
}
56+
for (const meta of metas) {
57+
const codeActionsForMeta = meta.getCodeActions({
58+
templateInfo,
59+
compiler,
60+
start,
61+
end,
62+
errorCode: code,
63+
formatOptions,
64+
preferences,
65+
tsLs: this.tsLS,
66+
});
67+
const fixAllAvailable = isFixAllAvailable(meta, diagnostics);
68+
const removeFixIdForCodeActions =
69+
codeActionsForMeta.map(({fixId, fixAllDescription, ...codeActionForMeta}) => {
70+
return fixAllAvailable ? {...codeActionForMeta, fixId, fixAllDescription} :
71+
codeActionForMeta;
72+
});
73+
codeActions.push(...removeFixIdForCodeActions);
74+
}
75+
}
76+
return codeActions;
77+
}
78+
79+
/**
80+
* When the user wants to fix the all same type of diagnostics in the `scope`, this function will
81+
* be called and fix all diagnostics which will be filtered by the `errorCodes` from the
82+
* `CodeActionMeta` that the `fixId` belongs to.
83+
*/
84+
getAllCodeActions(
85+
compiler: NgCompiler, diagnostics: tss.Diagnostic[], scope: tss.CombinedCodeFixScope,
86+
fixId: string, formatOptions: tss.FormatCodeSettings,
87+
preferences: tss.UserPreferences): tss.CombinedCodeActions {
88+
const meta = this.fixIdToRegistration.get(fixId as FixIdForCodeFixesAll);
89+
if (meta === undefined) {
90+
return {
91+
changes: [],
92+
};
93+
}
94+
return meta.getAllCodeActions({
95+
compiler,
96+
fixId,
97+
formatOptions,
98+
preferences,
99+
tsLs: this.tsLS,
100+
scope,
101+
diagnostics,
102+
});
103+
}
104+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {findFirstMatchingNode} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments';
10+
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
11+
import ts from 'typescript';
12+
import * as tss from 'typescript/lib/tsserverlibrary';
13+
14+
import {getTargetAtPosition, getTcbNodesOfTemplateAtPosition, TargetNodeKind} from '../template_target';
15+
import {getTemplateInfoAtPosition} from '../utils';
16+
17+
import {CodeActionMeta, convertFileTextChangeInTcb, FixIdForCodeFixesAll} from './utils';
18+
19+
const errorCodes: number[] = [
20+
2551, // https://github.com/microsoft/TypeScript/blob/8e6e87fea6463e153822e88431720f846c3b8dfa/src/compiler/diagnosticMessages.json#L2493
21+
2339, // https://github.com/microsoft/TypeScript/blob/8e6e87fea6463e153822e88431720f846c3b8dfa/src/compiler/diagnosticMessages.json#L1717
22+
];
23+
24+
/**
25+
* This code action will fix the missing member of a type. For example, add the missing member to
26+
* the type or try to get the spelling suggestion for the name from the type.
27+
*/
28+
export const missingMemberMeta: CodeActionMeta = {
29+
errorCodes,
30+
getCodeActions: function(
31+
{templateInfo, start, compiler, formatOptions, preferences, errorCode, tsLs}) {
32+
const tcbNodesInfo = getTcbNodesOfTemplateAtPosition(templateInfo, start, compiler);
33+
if (tcbNodesInfo === null) {
34+
return [];
35+
}
36+
37+
const codeActions: ts.CodeFixAction[] = [];
38+
const tcb = tcbNodesInfo.componentTcbNode;
39+
for (const tcbNode of tcbNodesInfo.nodes) {
40+
const tsLsCodeActions = tsLs.getCodeFixesAtPosition(
41+
tcb.getSourceFile().fileName, tcbNode.getStart(), tcbNode.getEnd(), [errorCode],
42+
formatOptions, preferences);
43+
codeActions.push(...tsLsCodeActions);
44+
}
45+
return codeActions.map(codeAction => {
46+
return {
47+
fixName: codeAction.fixName,
48+
fixId: codeAction.fixId,
49+
fixAllDescription: codeAction.fixAllDescription,
50+
description: codeAction.description,
51+
changes: convertFileTextChangeInTcb(codeAction.changes, compiler),
52+
commands: codeAction.commands,
53+
};
54+
});
55+
},
56+
fixIds: [FixIdForCodeFixesAll.FIX_SPELLING, FixIdForCodeFixesAll.FIX_MISSING_MEMBER],
57+
getAllCodeActions: function(
58+
{tsLs, scope, fixId, formatOptions, preferences, compiler, diagnostics}) {
59+
const changes: tss.FileTextChanges[] = [];
60+
const seen: Set<tss.ClassDeclaration> = new Set();
61+
for (const diag of diagnostics) {
62+
if (!errorCodes.includes(diag.code)) {
63+
continue;
64+
}
65+
66+
const fileName = diag.file?.fileName;
67+
if (fileName === undefined) {
68+
continue;
69+
}
70+
if (diag.start === undefined) {
71+
continue;
72+
}
73+
const componentClass = getTemplateInfoAtPosition(fileName, diag.start, compiler)?.component;
74+
if (componentClass === undefined) {
75+
continue;
76+
}
77+
if (seen.has(componentClass)) {
78+
continue;
79+
}
80+
seen.add(componentClass);
81+
82+
const tcb = compiler.getTemplateTypeChecker().getTypeCheckBlock(componentClass);
83+
if (tcb === null) {
84+
continue;
85+
}
86+
87+
const combinedCodeActions = tsLs.getCombinedCodeFix(
88+
{
89+
type: scope.type,
90+
fileName: tcb.getSourceFile().fileName,
91+
},
92+
fixId, formatOptions, preferences);
93+
changes.push(...combinedCodeActions.changes);
94+
}
95+
return {
96+
changes: convertFileTextChangeInTcb(changes, compiler),
97+
};
98+
}
99+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export {ALL_CODE_FIXES_METAS} from './all_codefixes_metas';
10+
export {CodeFixes} from './code_fixes';
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {absoluteFrom} from '@angular/compiler-cli';
10+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
11+
import * as tss from 'typescript/lib/tsserverlibrary';
12+
13+
import {TemplateInfo} from '../utils';
14+
15+
/**
16+
* This context is the info includes the `errorCode` at the given span the user selected in the
17+
* editor and the `NgCompiler` could help to fix it.
18+
*
19+
* When the editor tries to provide a code fix for a diagnostic in a span of a template file, this
20+
* context will be provided to the `CodeActionMeta` which could handle the `errorCode`.
21+
*/
22+
export interface CodeActionContext {
23+
templateInfo: TemplateInfo;
24+
compiler: NgCompiler;
25+
start: number;
26+
end: number;
27+
errorCode: number;
28+
formatOptions: tss.FormatCodeSettings;
29+
preferences: tss.UserPreferences;
30+
tsLs: tss.LanguageService;
31+
}
32+
33+
/**
34+
* This context is the info includes all diagnostics in the `scope` and the `NgCompiler` that could
35+
* help to fix it.
36+
*
37+
* When the editor tries to fix the all same type of diagnostics selected by the user in the
38+
* `scope`, this context will be provided to the `CodeActionMeta` which could handle the `fixId`.
39+
*/
40+
export interface CodeFixAllContext {
41+
scope: tss.CombinedCodeFixScope;
42+
compiler: NgCompiler;
43+
// https://github.com/microsoft/TypeScript/blob/5c4caafc2a2d0fceb03fce80fb14d3ee4407d918/src/services/types.ts#L781-L785
44+
fixId: string;
45+
formatOptions: tss.FormatCodeSettings;
46+
preferences: tss.UserPreferences;
47+
tsLs: tss.LanguageService;
48+
diagnostics: tss.Diagnostic[];
49+
}
50+
51+
export interface CodeActionMeta {
52+
errorCodes: Array<number>;
53+
getCodeActions: (context: CodeActionContext) => readonly tss.CodeFixAction[];
54+
fixIds: FixIdForCodeFixesAll[];
55+
getAllCodeActions: (context: CodeFixAllContext) => tss.CombinedCodeActions;
56+
}
57+
58+
/**
59+
* Convert the span of `textChange` in the TCB to the span of the template.
60+
*/
61+
export function convertFileTextChangeInTcb(
62+
changes: readonly tss.FileTextChanges[], compiler: NgCompiler): tss.FileTextChanges[] {
63+
const ttc = compiler.getTemplateTypeChecker();
64+
const fileTextChanges: tss.FileTextChanges[] = [];
65+
for (const fileTextChange of changes) {
66+
if (!ttc.isTrackedTypeCheckFile(absoluteFrom(fileTextChange.fileName))) {
67+
fileTextChanges.push(fileTextChange);
68+
continue;
69+
}
70+
const textChanges: tss.TextChange[] = [];
71+
let fileName: string|undefined;
72+
const seenTextChangeInTemplate = new Set<string>();
73+
for (const textChange of fileTextChange.textChanges) {
74+
const templateMap = ttc.getTemplateMappingAtTcbLocation({
75+
tcbPath: absoluteFrom(fileTextChange.fileName),
76+
isShimFile: true,
77+
positionInFile: textChange.span.start,
78+
});
79+
if (templateMap === null) {
80+
continue;
81+
}
82+
const mapping = templateMap.templateSourceMapping;
83+
if (mapping.type === 'external') {
84+
fileName = mapping.templateUrl;
85+
} else if (mapping.type === 'direct') {
86+
fileName = mapping.node.getSourceFile().fileName;
87+
} else {
88+
continue;
89+
}
90+
const start = templateMap.span.start.offset;
91+
const length = templateMap.span.end.offset - templateMap.span.start.offset;
92+
const changeSpanKey = `${start},${length}`;
93+
if (seenTextChangeInTemplate.has(changeSpanKey)) {
94+
continue;
95+
}
96+
seenTextChangeInTemplate.add(changeSpanKey);
97+
textChanges.push({
98+
newText: textChange.newText,
99+
span: {
100+
start,
101+
length,
102+
},
103+
});
104+
}
105+
if (fileName === undefined) {
106+
continue;
107+
}
108+
fileTextChanges.push({
109+
fileName,
110+
isNewFile: fileTextChange.isNewFile,
111+
textChanges,
112+
});
113+
}
114+
return fileTextChanges;
115+
}
116+
117+
/**
118+
* 'fix all' is only available when there are multiple diagnostics that the code action meta
119+
* indicates it can fix.
120+
*/
121+
export function isFixAllAvailable(meta: CodeActionMeta, diagnostics: tss.Diagnostic[]) {
122+
const errorCodes = meta.errorCodes;
123+
let maybeFixableDiagnostics = 0;
124+
for (const diag of diagnostics) {
125+
if (errorCodes.includes(diag.code)) maybeFixableDiagnostics++;
126+
if (maybeFixableDiagnostics > 1) return true;
127+
}
128+
129+
return false;
130+
}
131+
132+
export enum FixIdForCodeFixesAll {
133+
FIX_SPELLING = 'fixSpelling',
134+
FIX_MISSING_MEMBER = 'fixMissingMember',
135+
}

0 commit comments

Comments
 (0)