Skip to content

Commit 5d2e859

Browse files
ivanwondercrisbeto
authored andcommitted
feat(language-service): support to fix missing required inputs diagnostic (#50911)
Support to add the missing required inputs into the element and append after the last attribute of the element. But it skips the structural directive shorthand attribute. For example, `<a *ngFor=""></a>`, its shorthand is `<ng-template ngFor>`, the `valueSpan` of the `ngFor` is empty, and the info is lost, so it can't use to insert the missing attribute after it. PR Close #50911
1 parent 0cd6f36 commit 5d2e859

File tree

5 files changed

+551
-0
lines changed

5 files changed

+551
-0
lines changed

packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export interface TemplateTypeChecker {
139139
*/
140140
getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol | null;
141141
getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol | null;
142+
getSymbolOfNode(
143+
node: TmplAstTemplate | TmplAstElement,
144+
component: ts.ClassDeclaration,
145+
): TemplateSymbol | ElementSymbol | null;
142146
getSymbolOfNode(
143147
node: TmplAstComponent,
144148
component: ts.ClassDeclaration,

packages/language-service/src/codefixes/all_codefixes_metas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import {fixInvalidBananaInBoxMeta} from './fix_invalid_banana_in_box';
1010
import {missingImportMeta} from './fix_missing_import';
1111
import {missingMemberMeta} from './fix_missing_member';
1212
import {fixUnusedStandaloneImportsMeta} from './fix_unused_standalone_imports';
13+
import {fixMissingRequiredInput} from './fix_missing_required_inputs';
1314
import {CodeActionMeta} from './utils';
1415

1516
export const ALL_CODE_FIXES_METAS: CodeActionMeta[] = [
1617
missingMemberMeta,
1718
fixInvalidBananaInBoxMeta,
1819
missingImportMeta,
1920
fixUnusedStandaloneImportsMeta,
21+
fixMissingRequiredInput,
2022
];
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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 {
10+
BindingType,
11+
TmplAstBoundAttribute,
12+
TmplAstElement,
13+
TmplAstNode,
14+
TmplAstTemplate,
15+
TmplAstTextAttribute,
16+
} from '@angular/compiler';
17+
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
18+
import {TypeCheckableDirectiveMeta} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
19+
import ts from 'typescript';
20+
21+
import {getTargetAtPosition, TargetNodeKind} from '../template_target';
22+
23+
import {CodeActionContext, CodeActionMeta, FixIdForCodeFixesAll} from './utils';
24+
25+
/**
26+
* This code action will fix the missing required input of an element.
27+
*/
28+
export const fixMissingRequiredInput: CodeActionMeta = {
29+
errorCodes: [ngErrorCode(ErrorCode.MISSING_REQUIRED_INPUTS)],
30+
getCodeActions: function ({typeCheckInfo, start, compiler, fileName}: CodeActionContext) {
31+
if (typeCheckInfo === null) {
32+
return [];
33+
}
34+
35+
const positionDetails = getTargetAtPosition(typeCheckInfo.nodes, start);
36+
if (positionDetails === null) {
37+
return [];
38+
}
39+
40+
// For two-way bindings, we actually only need to be concerned with the bound attribute because
41+
// the bindings in the template are written with the attribute name, not the event name.
42+
const node =
43+
positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext
44+
? positionDetails.context.nodes[0]
45+
: positionDetails.context.node;
46+
47+
if (!(node instanceof TmplAstElement || node instanceof TmplAstTemplate)) {
48+
return [];
49+
}
50+
51+
let tagName: string | null = null;
52+
if (node instanceof TmplAstElement) {
53+
tagName = node.name;
54+
} else {
55+
tagName = node.tagName;
56+
}
57+
if (tagName === null) {
58+
return [];
59+
}
60+
61+
let insertPosition = node.startSourceSpan.start.offset + tagName.length + 1;
62+
const lastAttribute = findLastAttributeInTheElement(node);
63+
if (lastAttribute !== null) {
64+
insertPosition = lastAttribute.sourceSpan.end.offset;
65+
}
66+
67+
const ttc = compiler.getTemplateTypeChecker();
68+
69+
const symbol = ttc.getSymbolOfNode(node, typeCheckInfo.declaration);
70+
if (symbol === null) {
71+
return [];
72+
}
73+
74+
const codeActions: ts.CodeFixAction[] = [];
75+
76+
for (const dirSymbol of symbol.directives) {
77+
const directive = dirSymbol.tsSymbol.valueDeclaration;
78+
if (!ts.isClassDeclaration(directive)) {
79+
continue;
80+
}
81+
82+
const meta = ttc.getDirectiveMetadata(directive);
83+
if (meta === null) {
84+
continue;
85+
}
86+
87+
const seenRequiredInputs = new Set<string>();
88+
89+
const boundAttrs = getBoundAttributes(meta, node);
90+
for (const attr of boundAttrs) {
91+
for (const {fieldName, required} of attr.inputs) {
92+
if (required) {
93+
seenRequiredInputs.add(fieldName);
94+
}
95+
}
96+
}
97+
98+
for (const input of meta.inputs) {
99+
if (!input.required || seenRequiredInputs.has(input.classPropertyName)) {
100+
continue;
101+
}
102+
const typeCheck = compiler.getCurrentProgram().getTypeChecker();
103+
const memberSymbol = typeCheck.getPropertyOfType(dirSymbol.tsType, input.classPropertyName);
104+
if (memberSymbol === undefined) {
105+
continue;
106+
}
107+
108+
// As a general solution, always offer a property binding suggestion (e.g., `[inputName]=""`).
109+
// This is the most versatile way for users to satisfy a required input,
110+
// allowing them to bind to component properties or provide initial literal values.
111+
const insertBoundText = `[${input.bindingPropertyName}]=""`;
112+
codeActions.push({
113+
fixName: FixIdForCodeFixesAll.FIX_MISSING_REQUIRED_INPUTS,
114+
// fixId: FixIdForCodeFixesAll.FIX_MISSING_REQUIRED_INPUTS,
115+
// fixAllDescription: '',
116+
description: `Create ${insertBoundText} attribute for "${tagName}"`,
117+
changes: [
118+
{
119+
fileName,
120+
textChanges: [
121+
{
122+
span: ts.createTextSpan(insertPosition, 0),
123+
newText: ' ' + insertBoundText,
124+
},
125+
],
126+
},
127+
],
128+
});
129+
}
130+
}
131+
132+
return codeActions;
133+
},
134+
fixIds: [FixIdForCodeFixesAll.FIX_MISSING_REQUIRED_INPUTS],
135+
getAllCodeActions: function () {
136+
return {
137+
changes: [],
138+
};
139+
},
140+
};
141+
142+
interface TcbBoundAttribute {
143+
attribute: TmplAstBoundAttribute | TmplAstTextAttribute;
144+
inputs: {
145+
fieldName: string;
146+
required: boolean;
147+
}[];
148+
}
149+
150+
function getBoundAttributes(
151+
directive: TypeCheckableDirectiveMeta,
152+
node: TmplAstTemplate | TmplAstElement,
153+
): TcbBoundAttribute[] {
154+
const boundInputs: TcbBoundAttribute[] = [];
155+
156+
const processAttribute = (attr: TmplAstBoundAttribute | TmplAstTextAttribute) => {
157+
// Skip non-property bindings.
158+
if (attr instanceof TmplAstBoundAttribute && attr.type !== BindingType.Property) {
159+
return;
160+
}
161+
162+
// Skip the attribute if the directive does not have an input for it.
163+
const inputs = directive.inputs.getByBindingPropertyName(attr.name);
164+
165+
if (inputs !== null) {
166+
boundInputs.push({
167+
attribute: attr,
168+
inputs: inputs.map((input) => ({
169+
fieldName: input.classPropertyName,
170+
required: input.required,
171+
})),
172+
});
173+
}
174+
};
175+
176+
node.inputs.forEach(processAttribute);
177+
node.attributes.forEach(processAttribute);
178+
if (node instanceof TmplAstTemplate) {
179+
node.templateAttrs.forEach(processAttribute);
180+
}
181+
182+
return boundInputs;
183+
}
184+
185+
/**
186+
* If the last attribute is from the structural directive, it is skipped and returns
187+
* the previous attribute.
188+
*/
189+
function findLastAttributeInTheElement(
190+
element: TmplAstElement | TmplAstTemplate,
191+
): TmplAstNode | null {
192+
let lastAttribute: TmplAstNode | null = null;
193+
194+
const updateAttribute = (attr: TmplAstNode) => {
195+
if (lastAttribute === null) {
196+
lastAttribute = attr;
197+
return;
198+
}
199+
if (attr.sourceSpan.end.offset < lastAttribute.sourceSpan.end.offset) {
200+
return;
201+
}
202+
lastAttribute = attr;
203+
};
204+
205+
const attrNodes = [...element.attributes, ...element.inputs, ...element.outputs];
206+
207+
for (const attr of attrNodes) {
208+
updateAttribute(attr);
209+
}
210+
211+
return lastAttribute;
212+
}

packages/language-service/src/codefixes/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,5 @@ export enum FixIdForCodeFixesAll {
138138
FIX_INVALID_BANANA_IN_BOX = 'fixInvalidBananaInBox',
139139
FIX_MISSING_IMPORT = 'fixMissingImport',
140140
FIX_UNUSED_STANDALONE_IMPORTS = 'fixUnusedStandaloneImports',
141+
FIX_MISSING_REQUIRED_INPUTS = 'fixMissingRequiredInputs',
141142
}

0 commit comments

Comments
 (0)