Skip to content

Commit 3d2b92c

Browse files
authored
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
1 parent d96be35 commit 3d2b92c

16 files changed

+437
-58
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11964,7 +11964,7 @@ namespace ts {
1196411964
if (prop) {
1196511965
if (accessExpression) {
1196611966
markPropertyAsReferenced(prop, accessExpression, /*isThisAccess*/ accessExpression.expression.kind === SyntaxKind.ThisKeyword);
11967-
if (isAssignmentTarget(accessExpression) && (isReferenceToReadonlyEntity(accessExpression, prop) || isReferenceThroughNamespaceImport(accessExpression))) {
11967+
if (isAssignmentToReadonlyEntity(accessExpression, prop, getAssignmentTargetKind(accessExpression))) {
1196811968
error(accessExpression.argumentExpression, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop));
1196911969
return undefined;
1197011970
}
@@ -23046,11 +23046,9 @@ namespace ts {
2304623046
markPropertyAsReferenced(prop, node, left.kind === SyntaxKind.ThisKeyword);
2304723047
getNodeLinks(node).resolvedSymbol = prop;
2304823048
checkPropertyAccessibility(node, left.kind === SyntaxKind.SuperKeyword, apparentType, prop);
23049-
if (assignmentKind) {
23050-
if (isReferenceToReadonlyEntity(<Expression>node, prop) || isReferenceThroughNamespaceImport(<Expression>node)) {
23051-
error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right));
23052-
return errorType;
23053-
}
23049+
if (isAssignmentToReadonlyEntity(node as Expression, prop, assignmentKind)) {
23050+
error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right));
23051+
return errorType;
2305423052
}
2305523053
propType = getConstraintForLocation(getTypeOfSymbol(prop), node);
2305623054
}
@@ -26332,29 +26330,39 @@ namespace ts {
2633226330
);
2633326331
}
2633426332

26335-
function isReferenceToReadonlyEntity(expr: Expression, symbol: Symbol): boolean {
26333+
function isAssignmentToReadonlyEntity(expr: Expression, symbol: Symbol, assignmentKind: AssignmentKind) {
26334+
if (assignmentKind === AssignmentKind.None) {
26335+
// no assigment means it doesn't matter whether the entity is readonly
26336+
return false;
26337+
}
2633626338
if (isReadonlySymbol(symbol)) {
2633726339
// Allow assignments to readonly properties within constructors of the same class declaration.
2633826340
if (symbol.flags & SymbolFlags.Property &&
2633926341
(expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) &&
2634026342
(expr as AccessExpression).expression.kind === SyntaxKind.ThisKeyword) {
2634126343
// Look for if this is the constructor for the class that `symbol` is a property of.
26342-
const func = getContainingFunction(expr);
26343-
if (!(func && func.kind === SyntaxKind.Constructor)) {
26344+
const ctor = getContainingFunction(expr);
26345+
if (!(ctor && ctor.kind === SyntaxKind.Constructor)) {
2634426346
return true;
2634526347
}
26346-
// If func.parent is a class and symbol is a (readonly) property of that class, or
26347-
// if func is a constructor and symbol is a (readonly) parameter property declared in it,
26348-
// then symbol is writeable here.
26349-
return !symbol.valueDeclaration || !(func.parent === symbol.valueDeclaration.parent || func === symbol.valueDeclaration.parent);
26348+
if (symbol.valueDeclaration) {
26349+
const isAssignmentDeclaration = isBinaryExpression(symbol.valueDeclaration);
26350+
const isLocalPropertyDeclaration = ctor.parent === symbol.valueDeclaration.parent;
26351+
const isLocalParameterProperty = ctor === symbol.valueDeclaration.parent;
26352+
const isLocalThisPropertyAssignment = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor.parent;
26353+
const isLocalThisPropertyAssignmentConstructorFunction = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor;
26354+
const isWriteableSymbol =
26355+
isLocalPropertyDeclaration
26356+
|| isLocalParameterProperty
26357+
|| isLocalThisPropertyAssignment
26358+
|| isLocalThisPropertyAssignmentConstructorFunction;
26359+
return !isWriteableSymbol;
26360+
}
2635026361
}
2635126362
return true;
2635226363
}
26353-
return false;
26354-
}
26355-
26356-
function isReferenceThroughNamespaceImport(expr: Expression): boolean {
2635726364
if (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) {
26365+
// references through namespace import should be readonly
2635826366
const node = skipParentheses((expr as AccessExpression).expression);
2635926367
if (node.kind === SyntaxKind.Identifier) {
2636026368
const symbol = getNodeLinks(node).resolvedSymbol!;

src/compiler/parser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ namespace ts {
509509
case SyntaxKind.JSDocPublicTag:
510510
case SyntaxKind.JSDocPrivateTag:
511511
case SyntaxKind.JSDocProtectedTag:
512+
case SyntaxKind.JSDocReadonlyTag:
512513
return visitNode(cbNode, (node as JSDocTag).tagName);
513514
case SyntaxKind.PartiallyEmittedExpression:
514515
return visitNode(cbNode, (<PartiallyEmittedExpression>node).expression);
@@ -6845,6 +6846,9 @@ namespace ts {
68456846
case "protected":
68466847
tag = parseSimpleTag(start, SyntaxKind.JSDocProtectedTag, tagName);
68476848
break;
6849+
case "readonly":
6850+
tag = parseSimpleTag(start, SyntaxKind.JSDocReadonlyTag, tagName);
6851+
break;
68486852
case "this":
68496853
tag = parseThisTag(start, tagName);
68506854
break;

src/compiler/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ namespace ts {
471471
JSDocPublicTag,
472472
JSDocPrivateTag,
473473
JSDocProtectedTag,
474+
JSDocReadonlyTag,
474475
JSDocCallbackTag,
475476
JSDocEnumTag,
476477
JSDocParameterTag,
@@ -2632,6 +2633,10 @@ namespace ts {
26322633
kind: SyntaxKind.JSDocProtectedTag;
26332634
}
26342635

2636+
export interface JSDocReadonlyTag extends JSDocTag {
2637+
kind: SyntaxKind.JSDocReadonlyTag;
2638+
}
2639+
26352640
export interface JSDocEnumTag extends JSDocTag, Declaration {
26362641
parent: JSDoc;
26372642
kind: SyntaxKind.JSDocEnumTag;

src/compiler/utilities.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4130,7 +4130,8 @@ namespace ts {
41304130
// or when !(node.flags & NodeFlags.Synthesized) && node.kind !== SyntaxKind.SourceFile)
41314131
const tags = (getJSDocPublicTag(node) ? ModifierFlags.Public : ModifierFlags.None)
41324132
| (getJSDocPrivateTag(node) ? ModifierFlags.Private : ModifierFlags.None)
4133-
| (getJSDocProtectedTag(node) ? ModifierFlags.Protected : ModifierFlags.None);
4133+
| (getJSDocProtectedTag(node) ? ModifierFlags.Protected : ModifierFlags.None)
4134+
| (getJSDocReadonlyTag(node) ? ModifierFlags.Readonly : ModifierFlags.None);
41344135
flags |= tags;
41354136
}
41364137

src/compiler/utilitiesPublic.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,11 @@ namespace ts {
688688
return getFirstJSDocTag(node, isJSDocProtectedTag);
689689
}
690690

691+
/** Gets the JSDoc protected tag for the node if present */
692+
export function getJSDocReadonlyTag(node: Node): JSDocReadonlyTag | undefined {
693+
return getFirstJSDocTag(node, isJSDocReadonlyTag);
694+
}
695+
691696
/** Gets the JSDoc enum tag for the node if present */
692697
export function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined {
693698
return getFirstJSDocTag(node, isJSDocEnumTag);
@@ -1574,6 +1579,10 @@ namespace ts {
15741579
return node.kind === SyntaxKind.JSDocProtectedTag;
15751580
}
15761581

1582+
export function isJSDocReadonlyTag(node: Node): node is JSDocReadonlyTag {
1583+
return node.kind === SyntaxKind.JSDocReadonlyTag;
1584+
}
1585+
15771586
export function isJSDocEnumTag(node: Node): node is JSDocEnumTag {
15781587
return node.kind === SyntaxKind.JSDocEnumTag;
15791588
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -386,23 +386,24 @@ declare namespace ts {
386386
JSDocPublicTag = 308,
387387
JSDocPrivateTag = 309,
388388
JSDocProtectedTag = 310,
389-
JSDocCallbackTag = 311,
390-
JSDocEnumTag = 312,
391-
JSDocParameterTag = 313,
392-
JSDocReturnTag = 314,
393-
JSDocThisTag = 315,
394-
JSDocTypeTag = 316,
395-
JSDocTemplateTag = 317,
396-
JSDocTypedefTag = 318,
397-
JSDocPropertyTag = 319,
398-
SyntaxList = 320,
399-
NotEmittedStatement = 321,
400-
PartiallyEmittedExpression = 322,
401-
CommaListExpression = 323,
402-
MergeDeclarationMarker = 324,
403-
EndOfDeclarationMarker = 325,
404-
SyntheticReferenceExpression = 326,
405-
Count = 327,
389+
JSDocReadonlyTag = 311,
390+
JSDocCallbackTag = 312,
391+
JSDocEnumTag = 313,
392+
JSDocParameterTag = 314,
393+
JSDocReturnTag = 315,
394+
JSDocThisTag = 316,
395+
JSDocTypeTag = 317,
396+
JSDocTemplateTag = 318,
397+
JSDocTypedefTag = 319,
398+
JSDocPropertyTag = 320,
399+
SyntaxList = 321,
400+
NotEmittedStatement = 322,
401+
PartiallyEmittedExpression = 323,
402+
CommaListExpression = 324,
403+
MergeDeclarationMarker = 325,
404+
EndOfDeclarationMarker = 326,
405+
SyntheticReferenceExpression = 327,
406+
Count = 328,
406407
FirstAssignment = 62,
407408
LastAssignment = 74,
408409
FirstCompoundAssignment = 63,
@@ -431,9 +432,9 @@ declare namespace ts {
431432
LastStatement = 240,
432433
FirstNode = 152,
433434
FirstJSDocNode = 292,
434-
LastJSDocNode = 319,
435+
LastJSDocNode = 320,
435436
FirstJSDocTagNode = 304,
436-
LastJSDocTagNode = 319,
437+
LastJSDocTagNode = 320,
437438
}
438439
export enum NodeFlags {
439440
None = 0,
@@ -1632,6 +1633,9 @@ declare namespace ts {
16321633
export interface JSDocProtectedTag extends JSDocTag {
16331634
kind: SyntaxKind.JSDocProtectedTag;
16341635
}
1636+
export interface JSDocReadonlyTag extends JSDocTag {
1637+
kind: SyntaxKind.JSDocReadonlyTag;
1638+
}
16351639
export interface JSDocEnumTag extends JSDocTag, Declaration {
16361640
parent: JSDoc;
16371641
kind: SyntaxKind.JSDocEnumTag;
@@ -3472,6 +3476,8 @@ declare namespace ts {
34723476
function getJSDocPrivateTag(node: Node): JSDocPrivateTag | undefined;
34733477
/** Gets the JSDoc protected tag for the node if present */
34743478
function getJSDocProtectedTag(node: Node): JSDocProtectedTag | undefined;
3479+
/** Gets the JSDoc protected tag for the node if present */
3480+
function getJSDocReadonlyTag(node: Node): JSDocReadonlyTag | undefined;
34753481
/** Gets the JSDoc enum tag for the node if present */
34763482
function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined;
34773483
/** Gets the JSDoc this tag for the node if present */
@@ -3679,6 +3685,7 @@ declare namespace ts {
36793685
function isJSDocPublicTag(node: Node): node is JSDocPublicTag;
36803686
function isJSDocPrivateTag(node: Node): node is JSDocPrivateTag;
36813687
function isJSDocProtectedTag(node: Node): node is JSDocProtectedTag;
3688+
function isJSDocReadonlyTag(node: Node): node is JSDocReadonlyTag;
36823689
function isJSDocEnumTag(node: Node): node is JSDocEnumTag;
36833690
function isJSDocThisTag(node: Node): node is JSDocThisTag;
36843691
function isJSDocParameterTag(node: Node): node is JSDocParameterTag;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -386,23 +386,24 @@ declare namespace ts {
386386
JSDocPublicTag = 308,
387387
JSDocPrivateTag = 309,
388388
JSDocProtectedTag = 310,
389-
JSDocCallbackTag = 311,
390-
JSDocEnumTag = 312,
391-
JSDocParameterTag = 313,
392-
JSDocReturnTag = 314,
393-
JSDocThisTag = 315,
394-
JSDocTypeTag = 316,
395-
JSDocTemplateTag = 317,
396-
JSDocTypedefTag = 318,
397-
JSDocPropertyTag = 319,
398-
SyntaxList = 320,
399-
NotEmittedStatement = 321,
400-
PartiallyEmittedExpression = 322,
401-
CommaListExpression = 323,
402-
MergeDeclarationMarker = 324,
403-
EndOfDeclarationMarker = 325,
404-
SyntheticReferenceExpression = 326,
405-
Count = 327,
389+
JSDocReadonlyTag = 311,
390+
JSDocCallbackTag = 312,
391+
JSDocEnumTag = 313,
392+
JSDocParameterTag = 314,
393+
JSDocReturnTag = 315,
394+
JSDocThisTag = 316,
395+
JSDocTypeTag = 317,
396+
JSDocTemplateTag = 318,
397+
JSDocTypedefTag = 319,
398+
JSDocPropertyTag = 320,
399+
SyntaxList = 321,
400+
NotEmittedStatement = 322,
401+
PartiallyEmittedExpression = 323,
402+
CommaListExpression = 324,
403+
MergeDeclarationMarker = 325,
404+
EndOfDeclarationMarker = 326,
405+
SyntheticReferenceExpression = 327,
406+
Count = 328,
406407
FirstAssignment = 62,
407408
LastAssignment = 74,
408409
FirstCompoundAssignment = 63,
@@ -431,9 +432,9 @@ declare namespace ts {
431432
LastStatement = 240,
432433
FirstNode = 152,
433434
FirstJSDocNode = 292,
434-
LastJSDocNode = 319,
435+
LastJSDocNode = 320,
435436
FirstJSDocTagNode = 304,
436-
LastJSDocTagNode = 319,
437+
LastJSDocTagNode = 320,
437438
}
438439
export enum NodeFlags {
439440
None = 0,
@@ -1632,6 +1633,9 @@ declare namespace ts {
16321633
export interface JSDocProtectedTag extends JSDocTag {
16331634
kind: SyntaxKind.JSDocProtectedTag;
16341635
}
1636+
export interface JSDocReadonlyTag extends JSDocTag {
1637+
kind: SyntaxKind.JSDocReadonlyTag;
1638+
}
16351639
export interface JSDocEnumTag extends JSDocTag, Declaration {
16361640
parent: JSDoc;
16371641
kind: SyntaxKind.JSDocEnumTag;
@@ -3472,6 +3476,8 @@ declare namespace ts {
34723476
function getJSDocPrivateTag(node: Node): JSDocPrivateTag | undefined;
34733477
/** Gets the JSDoc protected tag for the node if present */
34743478
function getJSDocProtectedTag(node: Node): JSDocProtectedTag | undefined;
3479+
/** Gets the JSDoc protected tag for the node if present */
3480+
function getJSDocReadonlyTag(node: Node): JSDocReadonlyTag | undefined;
34753481
/** Gets the JSDoc enum tag for the node if present */
34763482
function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined;
34773483
/** Gets the JSDoc this tag for the node if present */
@@ -3679,6 +3685,7 @@ declare namespace ts {
36793685
function isJSDocPublicTag(node: Node): node is JSDocPublicTag;
36803686
function isJSDocPrivateTag(node: Node): node is JSDocPrivateTag;
36813687
function isJSDocProtectedTag(node: Node): node is JSDocProtectedTag;
3688+
function isJSDocReadonlyTag(node: Node): node is JSDocReadonlyTag;
36823689
function isJSDocEnumTag(node: Node): node is JSDocEnumTag;
36833690
function isJSDocThisTag(node: Node): node is JSDocThisTag;
36843691
function isJSDocParameterTag(node: Node): node is JSDocParameterTag;

tests/baselines/reference/jsDeclarationsClasses.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ export class E<T, U> {
477477
* @type {string}
478478
* @readonly
479479
*/
480-
static staticReadonlyField: string;
480+
static readonly staticReadonlyField: string;
481481
static staticInitializedField: number;
482482
/**
483483
* @param {string} _p
@@ -508,7 +508,7 @@ export class E<T, U> {
508508
* @type {T & U}
509509
* @readonly
510510
*/
511-
readonlyField: T & U;
511+
readonly readonlyField: T & U;
512512
initializedField: number;
513513
/**
514514
* @param {U} _p
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
tests/cases/conformance/jsdoc/jsdocReadonly.js(23,3): error TS2540: Cannot assign to 'y' because it is a read-only property.
2+
3+
4+
==== tests/cases/conformance/jsdoc/jsdocReadonly.js (1 errors) ====
5+
class LOL {
6+
/**
7+
* @readonly
8+
* @private
9+
* @type {number}
10+
* Order rules do not apply to JSDoc
11+
*/
12+
x = 1
13+
/** @readonly */
14+
y = 2
15+
/** @readonly Definitely not here */
16+
static z = 3
17+
/** @readonly This is OK too */
18+
constructor() {
19+
/** ok */
20+
this.y = 2
21+
/** @readonly ok */
22+
this.ka = 2
23+
}
24+
}
25+
26+
var l = new LOL()
27+
l.y = 12
28+
~
29+
!!! error TS2540: Cannot assign to 'y' because it is a read-only property.
30+

0 commit comments

Comments
 (0)