Skip to content

Commit

Permalink
Merge pull request #37894 from microsoft/always-error-on-property-ove…
Browse files Browse the repository at this point in the history
…rride-accessor

Always error on property override accessor
  • Loading branch information
sandersn committed May 26, 2020
2 parents 6ac6a04 + e466bb9 commit bfa1744
Show file tree
Hide file tree
Showing 15 changed files with 504 additions and 268 deletions.
5 changes: 2 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33824,8 +33824,7 @@ namespace ts {
const derivedPropertyFlags = derived.flags & SymbolFlags.PropertyOrAccessor;
if (basePropertyFlags && derivedPropertyFlags) {
// property/accessor is overridden with property/accessor
if (!compilerOptions.useDefineForClassFields
|| baseDeclarationFlags & ModifierFlags.Abstract && !(base.valueDeclaration && isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer)
if (baseDeclarationFlags & ModifierFlags.Abstract && !(base.valueDeclaration && isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer)
|| base.valueDeclaration && base.valueDeclaration.parent.kind === SyntaxKind.InterfaceDeclaration
|| derived.valueDeclaration && isBinaryExpression(derived.valueDeclaration)) {
// when the base property is abstract or from an interface, base/derived flags don't need to match
Expand All @@ -33841,7 +33840,7 @@ namespace ts {
Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor;
error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType), typeToString(type));
}
else {
else if (compilerOptions.useDefineForClassFields) {
const uninitialized = find(derived.declarations, d => d.kind === SyntaxKind.PropertyDeclaration && !(d as PropertyDeclaration).initializer);
if (uninitialized
&& !(derived.flags & SymbolFlags.Transient)
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5705,6 +5705,10 @@
"category": "Message",
"code": 95118
},
"Generate 'get' and 'set' accessors for all overriding properties": {
"category": "Message",
"code": 95119
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
13 changes: 0 additions & 13 deletions src/services/codefixes/fixAddMissingMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,6 @@ namespace ts.codefix {
},
});

function getAllSupers(decl: ClassOrInterface | undefined, checker: TypeChecker): readonly ClassOrInterface[] {
const res: ClassLikeDeclaration[] = [];
while (decl) {
const superElement = getClassExtendsHeritageElement(decl);
const superSymbol = superElement && checker.getSymbolAtLocation(superElement.expression);
const superDecl = superSymbol && find(superSymbol.declarations, isClassLike);
if (superDecl) { res.push(superDecl); }
decl = superDecl;
}
return res;
}

type ClassOrInterface = ClassLikeDeclaration | InterfaceDeclaration;
const enum InfoKind { Enum, ClassOrInterface }
interface EnumInfo {
readonly kind: InfoKind.Enum;
Expand Down
57 changes: 57 additions & 0 deletions src/services/codefixes/fixPropertyOverrideAccessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* @internal */
namespace ts.codefix {
const errorCodes = [
Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property.code,
Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor.code,
];
const fixId = "fixPropertyOverrideAccessor";
registerCodeFix({
errorCodes,
getCodeActions(context) {
const edits = doChange(context.sourceFile, context.span.start, context.span.length, context.errorCode, context);
if (edits) {
return [createCodeFixAction(fixId, edits, Diagnostics.Generate_get_and_set_accessors, fixId, Diagnostics.Generate_get_and_set_accessors_for_all_overriding_properties)];
}
},
fixIds: [fixId],

getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const edits = doChange(diag.file, diag.start, diag.length, diag.code, context);
if (edits) {
for (const edit of edits) {
changes.pushRaw(context.sourceFile, edit);
}
}
}),
});

function doChange(file: SourceFile, start: number, length: number, code: number, context: CodeFixContext | CodeFixAllContext) {
let startPosition: number;
let endPosition: number;
if (code === Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property.code) {
startPosition = start;
endPosition = start + length;
}
else if (code === Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor.code) {
const checker = context.program.getTypeChecker();
const node = getTokenAtPosition(file, start).parent;
Debug.assert(isAccessor(node), "error span of fixPropertyOverrideAccessor should only be on an accessor");
const containingClass = node.parent;
Debug.assert(isClassLike(containingClass), "erroneous accessors should only be inside classes");
const base = singleOrUndefined(getAllSupers(containingClass, checker));
if (!base) return [];

const name = unescapeLeadingUnderscores(getTextOfPropertyName(node.name));
const baseProp = checker.getPropertyOfType(checker.getTypeAtLocation(base), name);
if (!baseProp || !baseProp.valueDeclaration) return [];

startPosition = baseProp.valueDeclaration.pos;
endPosition = baseProp.valueDeclaration.end;
file = getSourceFileOfNode(baseProp.valueDeclaration);
}
else {
Debug.fail("fixPropertyOverrideAccessor codefix got unexpected error code " + code);
}
return generateAccessorFromProperty(file, startPosition, endPosition, context, Diagnostics.Generate_get_and_set_accessors.message);
}
}
243 changes: 243 additions & 0 deletions src/services/codefixes/generateAccessors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/* @internal */
namespace ts.codefix {
type AcceptedDeclaration = ParameterPropertyDeclaration | PropertyDeclaration | PropertyAssignment;
type AcceptedNameType = Identifier | StringLiteral;
type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression;

interface Info {
readonly container: ContainerDeclaration;
readonly isStatic: boolean;
readonly isReadonly: boolean;
readonly type: TypeNode | undefined;
readonly declaration: AcceptedDeclaration;
readonly fieldName: AcceptedNameType;
readonly accessorName: AcceptedNameType;
readonly originalName: string;
readonly renameAccessor: boolean;
}

export function generateAccessorFromProperty(file: SourceFile, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, start, end);
if (!fieldInfo) return undefined;

const changeTracker = textChanges.ChangeTracker.fromContext(context);
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;

suppressLeadingAndTrailingTrivia(fieldName);
suppressLeadingAndTrailingTrivia(accessorName);
suppressLeadingAndTrailingTrivia(declaration);
suppressLeadingAndTrailingTrivia(container);

let accessorModifiers: ModifiersArray | undefined;
let fieldModifiers: ModifiersArray | undefined;
if (isClassLike(container)) {
const modifierFlags = getEffectiveModifierFlags(declaration);
if (isSourceFileJS(file)) {
const modifiers = createModifiers(modifierFlags);
accessorModifiers = modifiers;
fieldModifiers = modifiers;
}
else {
accessorModifiers = createModifiers(prepareModifierFlagsForAccessor(modifierFlags));
fieldModifiers = createModifiers(prepareModifierFlagsForField(modifierFlags));
}
}

updateFieldDeclaration(changeTracker, file, declaration, fieldName, fieldModifiers);

const getAccessor = generateGetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container);
suppressLeadingAndTrailingTrivia(getAccessor);
insertAccessor(changeTracker, file, getAccessor, declaration, container);

if (isReadonly) {
// readonly modifier only existed in classLikeDeclaration
const constructor = getFirstConstructorWithBody(<ClassLikeDeclaration>container);
if (constructor) {
updateReadonlyPropertyInitializerStatementConstructor(changeTracker, file, constructor, fieldName.text, originalName);
}
}
else {
const setAccessor = generateSetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container);
suppressLeadingAndTrailingTrivia(setAccessor);
insertAccessor(changeTracker, file, setAccessor, declaration, container);
}

return changeTracker.getChanges();
}

function isConvertibleName(name: DeclarationName): name is AcceptedNameType {
return isIdentifier(name) || isStringLiteral(name);
}

function isAcceptedDeclaration(node: Node): node is AcceptedDeclaration {
return isParameterPropertyDeclaration(node, node.parent) || isPropertyDeclaration(node) || isPropertyAssignment(node);
}

function createPropertyName(name: string, originalName: AcceptedNameType) {
return isIdentifier(originalName) ? createIdentifier(name) : createLiteral(name);
}

function createAccessorAccessExpression(fieldName: AcceptedNameType, isStatic: boolean, container: ContainerDeclaration) {
const leftHead = isStatic ? (<ClassLikeDeclaration>container).name! : createThis(); // TODO: GH#18217
return isIdentifier(fieldName) ? createPropertyAccess(leftHead, fieldName) : createElementAccess(leftHead, createLiteral(fieldName));
}

function createModifiers(modifierFlags: ModifierFlags): ModifiersArray | undefined {
return modifierFlags ? createNodeArray(createModifiersFromModifierFlags(modifierFlags)) : undefined;
}

function prepareModifierFlagsForAccessor(modifierFlags: ModifierFlags): ModifierFlags {
modifierFlags &= ~ModifierFlags.Readonly; // avoid Readonly modifier because it will convert to get accessor
modifierFlags &= ~ModifierFlags.Private;

if (!(modifierFlags & ModifierFlags.Protected)) {
modifierFlags |= ModifierFlags.Public;
}

return modifierFlags;
}

function prepareModifierFlagsForField(modifierFlags: ModifierFlags): ModifierFlags {
modifierFlags &= ~ModifierFlags.Public;
modifierFlags &= ~ModifierFlags.Protected;
modifierFlags |= ModifierFlags.Private;
return modifierFlags;
}

export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, start: number, end: number): Info | undefined {
const node = getTokenAtPosition(file, start);
const declaration = findAncestor(node.parent, isAcceptedDeclaration);
// make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier
const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly;
if (!declaration || !nodeOverlapsWithStartEnd(declaration.name, file, start, end)
|| !isConvertibleName(declaration.name) || (getEffectiveModifierFlags(declaration) | meaning) !== meaning) return undefined;

const name = declaration.name.text;
const startWithUnderscore = startsWithUnderscore(name);
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
return {
isStatic: hasStaticModifier(declaration),
isReadonly: hasEffectiveReadonlyModifier(declaration),
type: getTypeAnnotationNode(declaration),
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
originalName: (<AcceptedNameType>declaration.name).text,
declaration,
fieldName,
accessorName,
renameAccessor: startWithUnderscore
};
}

function generateGetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) {
return createGetAccessor(
/*decorators*/ undefined,
modifiers,
accessorName,
/*parameters*/ undefined!, // TODO: GH#18217
type,
createBlock([
createReturn(
createAccessorAccessExpression(fieldName, isStatic, container)
)
], /*multiLine*/ true)
);
}

function generateSetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) {
return createSetAccessor(
/*decorators*/ undefined,
modifiers,
accessorName,
[createParameter(
/*decorators*/ undefined,
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
createIdentifier("value"),
/*questionToken*/ undefined,
type
)],
createBlock([
createStatement(
createAssignment(
createAccessorAccessExpression(fieldName, isStatic, container),
createIdentifier("value")
)
)
], /*multiLine*/ true)
);
}

function updatePropertyDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) {
const property = updateProperty(
declaration,
declaration.decorators,
modifiers,
fieldName,
declaration.questionToken || declaration.exclamationToken,
declaration.type,
declaration.initializer
);
changeTracker.replaceNode(file, declaration, property);
}

function updatePropertyAssignmentDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyAssignment, fieldName: AcceptedNameType) {
const assignment = updatePropertyAssignment(declaration, fieldName, declaration.initializer);
changeTracker.replacePropertyAssignment(file, declaration, assignment);
}

function updateFieldDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: AcceptedDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) {
if (isPropertyDeclaration(declaration)) {
updatePropertyDeclaration(changeTracker, file, declaration, fieldName, modifiers);
}
else if (isPropertyAssignment(declaration)) {
updatePropertyAssignmentDeclaration(changeTracker, file, declaration, fieldName);
}
else {
changeTracker.replaceNode(file, declaration,
updateParameter(declaration, declaration.decorators, modifiers, declaration.dotDotDotToken, cast(fieldName, isIdentifier), declaration.questionToken, declaration.type, declaration.initializer));
}
}

function insertAccessor(changeTracker: textChanges.ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) {
isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, <ClassLikeDeclaration>container, accessor) :
isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) :
changeTracker.insertNodeAfter(file, declaration, accessor);
}

function updateReadonlyPropertyInitializerStatementConstructor(changeTracker: textChanges.ChangeTracker, file: SourceFile, constructor: ConstructorDeclaration, fieldName: string, originalName: string) {
if (!constructor.body) return;
constructor.body.forEachChild(function recur(node) {
if (isElementAccessExpression(node) &&
node.expression.kind === SyntaxKind.ThisKeyword &&
isStringLiteral(node.argumentExpression) &&
node.argumentExpression.text === originalName &&
isWriteAccess(node)) {
changeTracker.replaceNode(file, node.argumentExpression, createStringLiteral(fieldName));
}
if (isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.ThisKeyword && node.name.text === originalName && isWriteAccess(node)) {
changeTracker.replaceNode(file, node.name, createIdentifier(fieldName));
}
if (!isFunctionLike(node) && !isClassLike(node)) {
node.forEachChild(recur);
}
});
}

export function getAllSupers(decl: ClassOrInterface | undefined, checker: TypeChecker): readonly ClassOrInterface[] {
const res: ClassLikeDeclaration[] = [];
while (decl) {
const superElement = getClassExtendsHeritageElement(decl);
const superSymbol = superElement && checker.getSymbolAtLocation(superElement.expression);
if (!superSymbol) break;
const symbol = superSymbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(superSymbol) : superSymbol;
const superDecl = find(symbol.declarations, isClassLike);
if (!superDecl) break;
res.push(superDecl);
decl = superDecl;
}
return res;
}

export type ClassOrInterface = ClassLikeDeclaration | InterfaceDeclaration;
}
Loading

0 comments on commit bfa1744

Please sign in to comment.