Merge pull request #37907 from Jack-Works/feat/class-to-classname

feat: add a codefix to fix class to className in react & add spelling suggest for JSX attributes
This commit is contained in:
Daniel Rosenwasser
2020-06-22 18:32:24 -07:00
committed by GitHub
11 changed files with 314 additions and 14 deletions
+21 -4
View File
@@ -606,6 +606,7 @@ namespace ts {
getAllPossiblePropertiesOfTypes,
getSuggestedSymbolForNonexistentProperty,
getSuggestionForNonexistentProperty,
getSuggestedSymbolForNonexistentJSXAttribute,
getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
getSuggestedSymbolForNonexistentModule,
@@ -16313,18 +16314,25 @@ namespace ts {
if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode) || isJsxOpeningLikeElement(errorNode.parent)) {
// JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal.
// However, using an object-literal error message will be very confusing to the users so we give different a message.
// TODO: Spelling suggestions for excess jsx attributes (needs new diagnostic messages)
if (prop.valueDeclaration && isJsxAttribute(prop.valueDeclaration) && getSourceFileOfNode(errorNode) === getSourceFileOfNode(prop.valueDeclaration.name)) {
// Note that extraneous children (as in `<NoChild>extra</NoChild>`) don't pass this check,
// since `children` is a SyntaxKind.PropertySignature instead of a SyntaxKind.JsxAttribute.
errorNode = prop.valueDeclaration.name;
}
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(errorTarget));
const propName = symbolToString(prop);
const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget);
const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined;
if (suggestion) {
reportError(Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion);
}
else {
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget));
}
}
else {
// use the property's value declaration if the property is assigned inside the literal itself
const objectLiteralDeclaration = source.symbol && firstOrUndefined(source.symbol.declarations);
let suggestion;
let suggestion: string | undefined;
if (prop.valueDeclaration && findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && getSourceFileOfNode(objectLiteralDeclaration) === getSourceFileOfNode(errorNode)) {
const propDeclaration = prop.valueDeclaration as ObjectLiteralElementLike;
Debug.assertNode(propDeclaration, isObjectLiteralElementLike);
@@ -24877,6 +24885,15 @@ namespace ts {
return getSpellingSuggestionForName(isString(name) ? name : idText(name), getPropertiesOfType(containingType), SymbolFlags.Value);
}
function getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined {
const strName = isString(name) ? name : idText(name);
const properties = getPropertiesOfType(containingType);
const jsxSpecific = strName === "for" ? find(properties, x => symbolName(x) === "htmlFor")
: strName === "class" ? find(properties, x => symbolName(x) === "className")
: undefined;
return jsxSpecific ?? getSpellingSuggestionForName(strName, properties, SymbolFlags.Value);
}
function getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined {
const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType);
return suggestion && symbolName(suggestion);
@@ -28223,7 +28240,7 @@ namespace ts {
error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference);
return booleanType;
}
if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateIdentifier(expr.name)) {
if (isPropertyAccessExpression(expr) && isPrivateIdentifier(expr.name)) {
error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier);
}
const links = getNodeLinks(expr);
+1
View File
@@ -4010,6 +4010,7 @@ namespace ts {
/* @internal */ tryGetMemberInModuleExportsAndProperties(memberName: string, moduleSymbol: Symbol): Symbol | undefined;
getApparentType(type: Type): Type;
/* @internal */ getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined;
/* @internal */ getSuggestedSymbolForNonexistentJSXAttribute(name: Identifier | string, containingType: Type): Symbol | undefined;
/* @internal */ getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined;
/* @internal */ getSuggestedSymbolForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined;
/* @internal */ getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined;
+19 -4
View File
@@ -7,12 +7,16 @@ namespace ts.codefix {
Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.code,
Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.code,
Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2.code,
// for JSX class components
Diagnostics.No_overload_matches_this_call.code,
// for JSX FC
Diagnostics.Type_0_is_not_assignable_to_type_1.code,
];
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { sourceFile } = context;
const info = getInfo(sourceFile, context.span.start, context);
const { sourceFile, errorCode } = context;
const info = getInfo(sourceFile, context.span.start, context, errorCode);
if (!info) return undefined;
const { node, suggestedSymbol } = info;
const { target } = context.host.getCompilationSettings();
@@ -21,18 +25,23 @@ namespace ts.codefix {
},
fixIds: [fixId],
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const info = getInfo(diag.file, diag.start, context);
const info = getInfo(diag.file, diag.start, context, diag.code);
const { target } = context.host.getCompilationSettings();
if (info) doChange(changes, context.sourceFile, info.node, info.suggestedSymbol, target!);
}),
});
function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase): { node: Node, suggestedSymbol: Symbol } | undefined {
function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase, errorCode: number): { node: Node, suggestedSymbol: Symbol } | undefined {
// This is the identifier of the misspelled word. eg:
// this.speling = 1;
// ^^^^^^^
const node = getTokenAtPosition(sourceFile, pos);
const parent = node.parent;
// Only fix spelling for No_overload_matches_this_call emitted on the React class component
if ((
errorCode === Diagnostics.No_overload_matches_this_call.code ||
errorCode === Diagnostics.Type_0_is_not_assignable_to_type_1.code) &&
!isJsxAttribute(parent)) return undefined;
const checker = context.program.getTypeChecker();
let suggestedSymbol: Symbol | undefined;
@@ -52,6 +61,12 @@ namespace ts.codefix {
suggestedSymbol = checker.getSuggestedSymbolForNonexistentModule(node, resolvedSourceFile.symbol);
}
}
else if (isJsxAttribute(parent) && parent.name === node) {
Debug.assertNode(node, isIdentifier, "Expected an identifier for JSX attribute");
const tag = findAncestor(node, isJsxOpeningLikeElement)!;
const props = checker.getContextualTypeForArgumentAtIndex(tag, 0);
suggestedSymbol = checker.getSuggestedSymbolForNonexistentJSXAttribute(node, props!);
}
else {
const meaning = getMeaningFromLocation(node);
const name = getTextOfNode(node);