Readonly support for jsdoc (#35790)

* Add @readonly

The rule for @readonly on this-assignments in the constructor is wrong.
See failing tests.

* In-progress

Add ctor function test
Add some notes and rename variable

* Done except for cleanup and fix 1 bug

* Fix last test and clean up
This commit is contained in:
Nathan Shively-Sanders
2019-12-20 13:47:20 -08:00
committed by GitHub
parent d96be353cb
commit 3d2b92ce69
16 changed files with 437 additions and 58 deletions

View File

@@ -11964,7 +11964,7 @@ namespace ts {
if (prop) {
if (accessExpression) {
markPropertyAsReferenced(prop, accessExpression, /*isThisAccess*/ accessExpression.expression.kind === SyntaxKind.ThisKeyword);
if (isAssignmentTarget(accessExpression) && (isReferenceToReadonlyEntity(accessExpression, prop) || isReferenceThroughNamespaceImport(accessExpression))) {
if (isAssignmentToReadonlyEntity(accessExpression, prop, getAssignmentTargetKind(accessExpression))) {
error(accessExpression.argumentExpression, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop));
return undefined;
}
@@ -23046,11 +23046,9 @@ namespace ts {
markPropertyAsReferenced(prop, node, left.kind === SyntaxKind.ThisKeyword);
getNodeLinks(node).resolvedSymbol = prop;
checkPropertyAccessibility(node, left.kind === SyntaxKind.SuperKeyword, apparentType, prop);
if (assignmentKind) {
if (isReferenceToReadonlyEntity(<Expression>node, prop) || isReferenceThroughNamespaceImport(<Expression>node)) {
error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right));
return errorType;
}
if (isAssignmentToReadonlyEntity(node as Expression, prop, assignmentKind)) {
error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right));
return errorType;
}
propType = getConstraintForLocation(getTypeOfSymbol(prop), node);
}
@@ -26332,29 +26330,39 @@ namespace ts {
);
}
function isReferenceToReadonlyEntity(expr: Expression, symbol: Symbol): boolean {
function isAssignmentToReadonlyEntity(expr: Expression, symbol: Symbol, assignmentKind: AssignmentKind) {
if (assignmentKind === AssignmentKind.None) {
// no assigment means it doesn't matter whether the entity is readonly
return false;
}
if (isReadonlySymbol(symbol)) {
// Allow assignments to readonly properties within constructors of the same class declaration.
if (symbol.flags & SymbolFlags.Property &&
(expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) &&
(expr as AccessExpression).expression.kind === SyntaxKind.ThisKeyword) {
// Look for if this is the constructor for the class that `symbol` is a property of.
const func = getContainingFunction(expr);
if (!(func && func.kind === SyntaxKind.Constructor)) {
const ctor = getContainingFunction(expr);
if (!(ctor && ctor.kind === SyntaxKind.Constructor)) {
return true;
}
// If func.parent is a class and symbol is a (readonly) property of that class, or
// if func is a constructor and symbol is a (readonly) parameter property declared in it,
// then symbol is writeable here.
return !symbol.valueDeclaration || !(func.parent === symbol.valueDeclaration.parent || func === symbol.valueDeclaration.parent);
if (symbol.valueDeclaration) {
const isAssignmentDeclaration = isBinaryExpression(symbol.valueDeclaration);
const isLocalPropertyDeclaration = ctor.parent === symbol.valueDeclaration.parent;
const isLocalParameterProperty = ctor === symbol.valueDeclaration.parent;
const isLocalThisPropertyAssignment = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor.parent;
const isLocalThisPropertyAssignmentConstructorFunction = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor;
const isWriteableSymbol =
isLocalPropertyDeclaration
|| isLocalParameterProperty
|| isLocalThisPropertyAssignment
|| isLocalThisPropertyAssignmentConstructorFunction;
return !isWriteableSymbol;
}
}
return true;
}
return false;
}
function isReferenceThroughNamespaceImport(expr: Expression): boolean {
if (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) {
// references through namespace import should be readonly
const node = skipParentheses((expr as AccessExpression).expression);
if (node.kind === SyntaxKind.Identifier) {
const symbol = getNodeLinks(node).resolvedSymbol!;

View File

@@ -509,6 +509,7 @@ namespace ts {
case SyntaxKind.JSDocPublicTag:
case SyntaxKind.JSDocPrivateTag:
case SyntaxKind.JSDocProtectedTag:
case SyntaxKind.JSDocReadonlyTag:
return visitNode(cbNode, (node as JSDocTag).tagName);
case SyntaxKind.PartiallyEmittedExpression:
return visitNode(cbNode, (<PartiallyEmittedExpression>node).expression);
@@ -6845,6 +6846,9 @@ namespace ts {
case "protected":
tag = parseSimpleTag(start, SyntaxKind.JSDocProtectedTag, tagName);
break;
case "readonly":
tag = parseSimpleTag(start, SyntaxKind.JSDocReadonlyTag, tagName);
break;
case "this":
tag = parseThisTag(start, tagName);
break;

View File

@@ -471,6 +471,7 @@ namespace ts {
JSDocPublicTag,
JSDocPrivateTag,
JSDocProtectedTag,
JSDocReadonlyTag,
JSDocCallbackTag,
JSDocEnumTag,
JSDocParameterTag,
@@ -2632,6 +2633,10 @@ namespace ts {
kind: SyntaxKind.JSDocProtectedTag;
}
export interface JSDocReadonlyTag extends JSDocTag {
kind: SyntaxKind.JSDocReadonlyTag;
}
export interface JSDocEnumTag extends JSDocTag, Declaration {
parent: JSDoc;
kind: SyntaxKind.JSDocEnumTag;

View File

@@ -4130,7 +4130,8 @@ namespace ts {
// or when !(node.flags & NodeFlags.Synthesized) && node.kind !== SyntaxKind.SourceFile)
const tags = (getJSDocPublicTag(node) ? ModifierFlags.Public : ModifierFlags.None)
| (getJSDocPrivateTag(node) ? ModifierFlags.Private : ModifierFlags.None)
| (getJSDocProtectedTag(node) ? ModifierFlags.Protected : ModifierFlags.None);
| (getJSDocProtectedTag(node) ? ModifierFlags.Protected : ModifierFlags.None)
| (getJSDocReadonlyTag(node) ? ModifierFlags.Readonly : ModifierFlags.None);
flags |= tags;
}

View File

@@ -688,6 +688,11 @@ namespace ts {
return getFirstJSDocTag(node, isJSDocProtectedTag);
}
/** Gets the JSDoc protected tag for the node if present */
export function getJSDocReadonlyTag(node: Node): JSDocReadonlyTag | undefined {
return getFirstJSDocTag(node, isJSDocReadonlyTag);
}
/** Gets the JSDoc enum tag for the node if present */
export function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined {
return getFirstJSDocTag(node, isJSDocEnumTag);
@@ -1574,6 +1579,10 @@ namespace ts {
return node.kind === SyntaxKind.JSDocProtectedTag;
}
export function isJSDocReadonlyTag(node: Node): node is JSDocReadonlyTag {
return node.kind === SyntaxKind.JSDocReadonlyTag;
}
export function isJSDocEnumTag(node: Node): node is JSDocEnumTag {
return node.kind === SyntaxKind.JSDocEnumTag;
}