-
Notifications
You must be signed in to change notification settings - Fork 24.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(language-service): support fix the component missing member
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
- Loading branch information
1 parent
a003dd8
commit 043ea8b
Showing
9 changed files
with
608 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
packages/language-service/src/codefixes/fix_missing_member.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {findFirstMatchingNode} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments'; | ||
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST | ||
import * as ts from 'typescript/lib/tsserverlibrary'; | ||
|
||
import {getTargetAtPosition, TargetNodeKind} from '../template_target'; | ||
import {getTemplateInfoAtPosition} from '../utils'; | ||
|
||
import {CodeActionMeta, convertTileTextChangeInTcb} from './utils'; | ||
|
||
const errorCodes = [ | ||
(ts as any).Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code, | ||
(ts as any).Diagnostics.Property_0_does_not_exist_on_type_1.code, | ||
]; | ||
|
||
export const missingMemberMeta: CodeActionMeta = { | ||
errorCodes, | ||
getCodeActions: function( | ||
{templateInfo, start, compiler, formatOptions, preferences, errorCode, tsLs}) { | ||
const target = getTargetAtPosition(templateInfo.template, start); | ||
if (!target) { | ||
return []; | ||
} | ||
let targetStart: number|undefined; | ||
let targetEnd: number|undefined; | ||
if (target.context.kind === TargetNodeKind.RawExpression) { | ||
const span = target.context.node; | ||
if (span instanceof e.PropertyRead) { | ||
targetStart = span.nameSpan.start; | ||
targetEnd = span.nameSpan.end; | ||
} | ||
} | ||
if (targetStart === undefined || targetEnd === undefined) { | ||
return []; | ||
} | ||
const tcb = compiler.getTemplateTypeChecker().getTypeCheckBlock(templateInfo.component); | ||
if (tcb === null) { | ||
return []; | ||
} | ||
const tcbNode = findFirstMatchingNode(tcb, { | ||
filter: (node): node is ts.PropertyAccessExpression => ts.isPropertyAccessExpression(node), | ||
withSpan: new e.AbsoluteSourceSpan(targetStart, targetEnd), | ||
}); | ||
if (tcbNode === null) { | ||
return []; | ||
} | ||
const codeActions = tsLs.getCodeFixesAtPosition( | ||
tcb.getSourceFile().fileName, tcbNode.name.getStart(), tcbNode.name.getEnd(), [errorCode], | ||
formatOptions, preferences); | ||
return codeActions.map(codeAction => { | ||
return { | ||
fixName: codeAction.fixName, | ||
fixId: codeAction.fixId, | ||
fixAllDescription: codeAction.fixAllDescription, | ||
description: codeAction.description, | ||
changes: convertTileTextChangeInTcb(codeAction.changes, compiler), | ||
commands: codeAction.commands, | ||
}; | ||
}); | ||
}, | ||
fixIds: ['fixSpelling', 'fixMissingMember'], | ||
getAllCodeActions: function( | ||
{tsLs, scope, fixId, formatOptions, preferences, compiler, diagnostics}) { | ||
let changes: ts.FileTextChanges[] = []; | ||
const seen: Set<ts.ClassDeclaration> = new Set(); | ||
for (const diag of diagnostics) { | ||
if (!errorCodes.includes(diag.code)) { | ||
continue; | ||
} | ||
|
||
const fileName = diag.file?.fileName; | ||
if (fileName === undefined) { | ||
continue; | ||
} | ||
if (diag.start === undefined) { | ||
continue; | ||
} | ||
const componentClass = getTemplateInfoAtPosition(fileName, diag.start, compiler)?.component; | ||
if (componentClass === undefined) { | ||
continue; | ||
} | ||
if (seen.has(componentClass)) { | ||
continue; | ||
} | ||
seen.add(componentClass); | ||
|
||
const tcb = compiler.getTemplateTypeChecker().getTypeCheckBlock(componentClass); | ||
if (tcb === null) { | ||
continue; | ||
} | ||
|
||
const combinedCodeActions = tsLs.getCombinedCodeFix( | ||
{ | ||
type: scope.type, | ||
fileName: tcb.getSourceFile().fileName, | ||
}, | ||
fixId, formatOptions, preferences); | ||
changes = changes.concat(combinedCodeActions.changes); | ||
} | ||
return { | ||
changes: convertTileTextChangeInTcb(changes, compiler), | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; | ||
import * as ts from 'typescript/lib/tsserverlibrary'; | ||
|
||
import {TemplateInfo} from '../utils'; | ||
|
||
import {missingMemberMeta} from './fix_missing_member'; | ||
import {CodeActionMeta, isFixAllUnavailable} from './utils'; | ||
|
||
export class CodeFixes { | ||
private errorCodeToFixes: Map<number, CodeActionMeta[]> = new Map(); | ||
private fixIdToRegistration = new Map<string, CodeActionMeta>(); | ||
|
||
constructor(private tsLS: ts.LanguageService) { | ||
const allFixesMeta = [missingMemberMeta]; | ||
for (const meta of allFixesMeta) { | ||
for (const err of meta.errorCodes) { | ||
let errMeta = this.errorCodeToFixes.get(err); | ||
if (errMeta === undefined) { | ||
this.errorCodeToFixes.set(err, errMeta = []); | ||
} | ||
errMeta.push(meta); | ||
} | ||
for (const fixId of meta.fixIds) { | ||
if (this.fixIdToRegistration.has(fixId)) { | ||
// https://github.com/microsoft/TypeScript/blob/28dc248e5c500c7be9a8c3a7341d303e026b023f/src/services/codeFixProvider.ts#L28 | ||
// In ts services, only one meta can be registered for a fixId. | ||
continue; | ||
} | ||
this.fixIdToRegistration.set(fixId, meta); | ||
} | ||
} | ||
} | ||
|
||
getCodeActions( | ||
templateInfo: TemplateInfo, compiler: NgCompiler, start: number, end: number, | ||
errorCodes: readonly number[], diagnostics: ts.Diagnostic[], | ||
formatOptions: ts.FormatCodeSettings, | ||
preferences: ts.UserPreferences): readonly ts.CodeFixAction[] { | ||
let codeActions: ts.CodeFixAction[] = []; | ||
for (const code of errorCodes) { | ||
const metas = this.errorCodeToFixes.get(code); | ||
if (metas === undefined) { | ||
continue; | ||
} | ||
for (const meta of metas) { | ||
const codeActionsForMeta = meta.getCodeActions({ | ||
templateInfo, | ||
compiler, | ||
start, | ||
end, | ||
errorCode: code, | ||
formatOptions, | ||
preferences, | ||
tsLs: this.tsLS, | ||
}); | ||
const fixAllUnavailable = isFixAllUnavailable(meta.errorCodes, diagnostics); | ||
codeActions = codeActions.concat( | ||
codeActionsForMeta.map(({fixId, fixAllDescription, ...codeActionForMeta}) => { | ||
return fixAllUnavailable ? codeActionForMeta : | ||
{...codeActionForMeta, fixId, fixAllDescription} | ||
})); | ||
} | ||
} | ||
return codeActions; | ||
} | ||
|
||
getAllCodeActions( | ||
compiler: NgCompiler, diagnostics: ts.Diagnostic[], scope: ts.CombinedCodeFixScope, | ||
fixId: string, formatOptions: ts.FormatCodeSettings, | ||
preferences: ts.UserPreferences): ts.CombinedCodeActions { | ||
const meta = this.fixIdToRegistration.get(fixId); | ||
if (meta === undefined) { | ||
return { | ||
changes: [], | ||
}; | ||
} | ||
return meta.getAllCodeActions({ | ||
compiler, | ||
fixId, | ||
formatOptions, | ||
preferences, | ||
tsLs: this.tsLS, | ||
scope, | ||
diagnostics, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {absoluteFrom} from '@angular/compiler-cli'; | ||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; | ||
import * as ts from 'typescript/lib/tsserverlibrary'; | ||
|
||
import {TemplateInfo} from '../utils'; | ||
|
||
export interface CodeActionContext { | ||
templateInfo: TemplateInfo; | ||
compiler: NgCompiler; | ||
start: number; | ||
end: number; | ||
errorCode: number; | ||
formatOptions: ts.FormatCodeSettings; | ||
preferences: ts.UserPreferences; | ||
tsLs: ts.LanguageService; | ||
} | ||
|
||
export interface CodeFixAllContext { | ||
scope: ts.CombinedCodeFixScope; | ||
compiler: NgCompiler; | ||
fixId: string; | ||
formatOptions: ts.FormatCodeSettings; | ||
preferences: ts.UserPreferences; | ||
tsLs: ts.LanguageService; | ||
diagnostics: ts.Diagnostic[]; | ||
} | ||
|
||
export interface CodeActionMeta { | ||
errorCodes: Array<number>; | ||
getCodeActions: (context: CodeActionContext) => readonly ts.CodeFixAction[]; | ||
fixIds: string[]; | ||
getAllCodeActions: (context: CodeFixAllContext) => ts.CombinedCodeActions; | ||
} | ||
|
||
export function convertTileTextChangeInTcb( | ||
changes: readonly ts.FileTextChanges[], compiler: NgCompiler): ts.FileTextChanges[] { | ||
const ttc = compiler.getTemplateTypeChecker(); | ||
const fileTextChanges: ts.FileTextChanges[] = []; | ||
for (const fileTextChange of changes) { | ||
if (!ttc.isTrackedTypeCheckFile(absoluteFrom(fileTextChange.fileName))) { | ||
fileTextChanges.push(fileTextChange); | ||
continue; | ||
} | ||
const textChanges: ts.TextChange[] = []; | ||
let fileName: string|undefined; | ||
const seenTextChangeInTemp = new Set<string>(); | ||
for (const textChange of fileTextChange.textChanges) { | ||
const templateMap = ttc.getTemplateMappingAtTcbLocation({ | ||
tcbPath: absoluteFrom(fileTextChange.fileName), | ||
isShimFile: true, | ||
positionInFile: textChange.span.start, | ||
}); | ||
if (templateMap === null) { | ||
continue; | ||
} | ||
const mapping = templateMap.templateSourceMapping; | ||
if (mapping.type === 'external') { | ||
fileName = mapping.templateUrl; | ||
} else if (mapping.type === 'direct') { | ||
fileName = mapping.node.getSourceFile().fileName; | ||
} else { | ||
continue; | ||
} | ||
const start = templateMap.span.start.offset; | ||
const length = templateMap.span.end.offset - templateMap.span.start.offset; | ||
const changeSpanKey = `${start},${length}`; | ||
if (seenTextChangeInTemp.has(changeSpanKey)) { | ||
continue; | ||
} | ||
seenTextChangeInTemp.add(changeSpanKey); | ||
textChanges.push({ | ||
newText: textChange.newText, | ||
span: { | ||
start, | ||
length, | ||
}, | ||
}); | ||
} | ||
if (fileName === undefined) { | ||
continue; | ||
} | ||
fileTextChanges.push({ | ||
fileName, | ||
isNewFile: fileTextChange.isNewFile, | ||
textChanges, | ||
}); | ||
} | ||
return fileTextChanges; | ||
} | ||
|
||
export function isFixAllUnavailable(errorCodes: number[], diagnostics: ts.Diagnostic[]) { | ||
let maybeFixableDiagnostics = 0; | ||
for (const diag of diagnostics) { | ||
if (errorCodes.includes(diag.code)) maybeFixableDiagnostics++; | ||
if (maybeFixableDiagnostics > 1) break; | ||
} | ||
|
||
return maybeFixableDiagnostics < 2; | ||
} |
Oops, something went wrong.