diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index e61b3a7a635c2b19614ac16a395a48af0fc5a76c..f031ea7d96de7c9e28689b74fff64d6caf5593e1 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -1914,6 +1914,81 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.handlePropertyDeclarationForProp(node); this.handleSdkGlobalApi(node); this.handleObjectLiteralAssignmentToClass(node); + this.checkPropertyDeclarationReadonlyUsage(node); + } + + private checkPropertyDeclarationReadonlyUsage(propDecl: ts.PropertyDeclaration): void { + if (!this.options.arkts2) { + return; + } + + const { parent, name } = propDecl; + + if (!ts.isClassDeclaration(parent)) { + return; + } + + if (!parent.heritageClauses) { + return; + } + + const extendedIdent = parent.heritageClauses.at(0); + + if (!TsUtils.hasModifier(propDecl.modifiers, ts.SyntaxKind.ReadonlyKeyword)) { + return; + } + + const extendedProp = this.getExtendedProperty(name, extendedIdent); + if (!extendedProp) { + return; + } + + if (TsUtils.hasModifier(extendedProp.modifiers, ts.SyntaxKind.ReadonlyKeyword)) { + return; + } + + this.incrementCounters(propDecl, FaultID.NoClassSuperPropReadonly); + } + + private getExtendedProperty( + propertyName: ts.PropertyName, + extended: ts.HeritageClause | undefined + ): ts.PropertySignature | undefined { + if (!extended) { + return undefined; + } + + const extendedType = extended.types.at(0); + if (!extendedType) { + return undefined; + } + + const extendedIdent = extendedType.expression; + if (!ts.isIdentifier(extendedIdent)) { + return undefined; + } + + const extendedDeclaration = this.tsUtils.getDeclarationNode(extendedIdent); + if (!extendedDeclaration) { + return undefined; + } + + if (!ts.isInterfaceDeclaration(extendedDeclaration)) { + return undefined; + } + + for (const member of extendedDeclaration.members) { + if (!ts.isPropertySignature(member)) { + continue; + } + if (member.name.getText() !== propertyName.getText()) { + continue; + } + + return member; + } + + return undefined; } private handleSendableClassProperty(node: ts.PropertyDeclaration): void { @@ -6247,7 +6322,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.checkOnClickCallback(tsCallOrNewExpr); } -private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpression): void { + private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpression): void { if (!tsCallOrNewExpr.arguments || tsCallOrNewExpr.arguments.length === 0 && this.options.arkts2) { return; } @@ -6277,8 +6352,8 @@ private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpressi private checkAsyncOrPromiseFunction(callback: ts.ArrowFunction): void { const returnsPromise = this.checkReturnsPromise(callback); - const isAsync = callback.modifiers?.some((m) => { - return m.kind === ts.SyntaxKind.AsyncKeyword; + const isAsync = callback.modifiers?.some((m) => { + return m.kind === ts.SyntaxKind.AsyncKeyword; }); if (isAsync || returnsPromise) { @@ -6286,9 +6361,15 @@ private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpressi const endPos = callback.body.getEnd(); const errorNode = { - getStart: () => { return startPos; }, - getEnd: () => { return endPos; }, - getSourceFile: () => { return callback.getSourceFile(); } + getStart: () => { + return startPos; + }, + getEnd: () => { + return endPos; + }, + getSourceFile: () => { + return callback.getSourceFile(); + } } as ts.Node; this.incrementCounters(errorNode, FaultID.IncompationbleFunctionType); diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets index c2c357ee4cb4b59e0d55af1dea0d04b6c3ad3cdc..f8c9d694db662f0061ffd5c7f69f1dff31975541 100644 --- a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets @@ -80,4 +80,20 @@ class B2 extends B1 { class B3 extends B2 { readonly data: number= 2; // legal -} \ No newline at end of file +} + +interface NonReadonly { + v: number +} + +class B4 extends NonReadonly { + readonly v: number = 0; // error +} + +interface Readonly { + readonly v: number; +} + +class B5 extends Readonly { + readonly v: number = 0; // legal +} diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json index 4acc088d1da62353e56ced57f16b342de413cb78..948b846fe04969bf5ccbe8bd9dc4a18559ce0c2c 100644 --- a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json @@ -16,4 +16,4 @@ "mode": { "arkts2": "" } -} \ No newline at end of file +} diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json index 38793eb6dc3e52b401baf165892163179b46dcd4..45209a4cbf3e39508dec42e97f0a23d558205dc0 100644 --- a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json @@ -83,6 +83,16 @@ "suggest": "", "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", "severity": "ERROR" + }, + { + "line": 90, + "column": 5, + "endLine": 90, + "endColumn": 28, + "problem": "NoClassSuperPropReadonly", + "suggest": "", + "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", + "severity": "ERROR" } ] } \ No newline at end of file