From fbb5358ab9afc2ce6dc5771654d83ad2d24fc872 Mon Sep 17 00:00:00 2001 From: wangweiyuan Date: Wed, 11 Jun 2025 17:32:44 +0800 Subject: [PATCH] =?UTF-8?q?add=20ui-syntax-plugins=200611=202=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: wangweiyuan --- .../rules/build-root-node.ts | 3 +- .../rules/computed-decorator-check.ts | 4 +- .../ui-syntax-plugins/rules/index.ts | 4 + .../rules/reusableV2-decorator-check.ts | 8 +- .../rules/struct-property-optional.ts | 91 ++++++++++++ .../rules/validate-build-in-struct.ts | 39 ++++- .../rules/validate-decorator-target.ts | 137 ++++++++++++++++++ 7 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 arkui-plugins/ui-syntax-plugins/rules/struct-property-optional.ts create mode 100644 arkui-plugins/ui-syntax-plugins/rules/validate-decorator-target.ts diff --git a/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts b/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts index 39d8bb07a..99fea66cd 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts @@ -14,10 +14,9 @@ */ import * as arkts from '@koalaui/libarkts'; -import { getIdentifierName, getAnnotationUsage, PresetDecorators } from '../utils'; +import { getIdentifierName, getAnnotationUsage, PresetDecorators, BUILD_NAME } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; -const BUILD_NAME: string = 'build'; const BUILD_ROOT_NUM: number = 1; const STATEMENT_LENGTH: number = 1; diff --git a/arkui-plugins/ui-syntax-plugins/rules/computed-decorator-check.ts b/arkui-plugins/ui-syntax-plugins/rules/computed-decorator-check.ts index 1d67fb928..f13ea578a 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/computed-decorator-check.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/computed-decorator-check.ts @@ -14,11 +14,9 @@ */ import * as arkts from '@koalaui/libarkts'; -import { getIdentifierName, PresetDecorators, getAnnotationName, getAnnotationUsage } from '../utils'; +import { getIdentifierName, PresetDecorators, getAnnotationName, getAnnotationUsage, BUILD_NAME } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; -const BUILD_NAME: string = 'build'; - function validateStructBody( node: arkts.StructDeclaration, computedGetters: Map, diff --git a/arkui-plugins/ui-syntax-plugins/rules/index.ts b/arkui-plugins/ui-syntax-plugins/rules/index.ts index 13b008b62..e854e1e4b 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/index.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/index.ts @@ -39,10 +39,12 @@ import NoSameAsBuiltInAttribute from './no-same-as-built-in-attribute'; import ReuseAttributeCheck from './reuse-attribute-check'; import StructMissingDecorator from './struct-missing-decorator'; import StructPropertyDecorator from './struct-property-decorator'; +import StructPropertyOptional from './struct-property-optional'; import StructVariableInitialization from './struct-variable-initialization'; import TrackDecoratorCheck from './track-decorator-check'; import TypeDecoratorCheck from './type-decorator-check'; import ValidateBuildInStruct from './validate-build-in-struct'; +import ValidateDecoratorTarget from './validate-decorator-target'; import VariableInitializationViaComponentCons from './variable-initialization-via-component-cons'; import WatchDecoratorFunction from './watch-decorator-function'; import WatchDecoratorRegular from './watch-decorator-regular'; @@ -80,10 +82,12 @@ const rules: UISyntaxRule[] = [ ReuseAttributeCheck, StructMissingDecorator, StructPropertyDecorator, + StructPropertyOptional, StructVariableInitialization, TrackDecoratorCheck, TypeDecoratorCheck, ValidateBuildInStruct, + ValidateDecoratorTarget, VariableInitializationViaComponentCons, WatchDecoratorFunction, WatchDecoratorRegular, diff --git a/arkui-plugins/ui-syntax-plugins/rules/reusableV2-decorator-check.ts b/arkui-plugins/ui-syntax-plugins/rules/reusableV2-decorator-check.ts index 41bee3a60..104c68ff2 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/reusableV2-decorator-check.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/reusableV2-decorator-check.ts @@ -17,7 +17,7 @@ import * as arkts from '@koalaui/libarkts'; import { PresetDecorators, getAnnotationUsage } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; -function reportInvalidDecoratorUsage( +function reportConflictingDecorators( reusableDocoratorUsage: arkts.AnnotationUsage | undefined, structNode: arkts.Identifier | undefined, context: UISyntaxRuleContext @@ -31,7 +31,7 @@ function reportInvalidDecoratorUsage( }); } -function reportConflictingDecorators( +function reportInvalidDecoratorUsage( node: arkts.StructDeclaration, structNode: arkts.Identifier | undefined, context: UISyntaxRuleContext @@ -70,11 +70,11 @@ const rule: UISyntaxRule = { const componnetV2DocoratorUsage = getAnnotationUsage(node, PresetDecorators.COMPONENT_V2); // Check whether @Reusable and @ReusableV2 exist at the same time if (reusableV2DocoratorUsage && reusableDocoratorUsage && structNode) { - reportInvalidDecoratorUsage(reusableDocoratorUsage, structNode, context); + reportConflictingDecorators(reusableDocoratorUsage, structNode, context); } // Check if @ReusableV2 is applied to a class decorated by @ComponentV2 if (reusableV2DocoratorUsage && !componnetV2DocoratorUsage && structNode) { - reportConflictingDecorators(node, structNode, context); + reportInvalidDecoratorUsage(node, structNode, context); } }, }; diff --git a/arkui-plugins/ui-syntax-plugins/rules/struct-property-optional.ts b/arkui-plugins/ui-syntax-plugins/rules/struct-property-optional.ts new file mode 100644 index 000000000..de7f40b21 --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/struct-property-optional.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; +import { PresetDecorators } from '../utils/index'; + +// @Prop needs to consider whether there is an initialization value +const requireDecorators = [ + PresetDecorators.LINK, + PresetDecorators.OBJECT_LINK, +]; + +function hasPropOrRequireDecorator(context: UISyntaxRuleContext, node: arkts.ClassProperty, propertyName: string): void { + node.annotations?.forEach(annotation => { + if (annotation.expr && arkts.isIdentifier(annotation.expr)) { + const decoratorName = annotation.expr?.name; + const nodeKey = node.key; + const nodeValue = node.value; + // Check whether the prop decorator has an initial value, and no alarm will be generated if there is an initial value + if (decoratorName === PresetDecorators.PROP && nodeKey && !nodeValue) { + context.report({ + node: nodeKey, + message: rule.messages.propertyOptional, + data: { + decoratorName, + propertyName, + }, + }); + } else if (requireDecorators.includes(decoratorName) && nodeKey) { + context.report({ + node: nodeKey, + message: rule.messages.propertyOptional, + data: { + decoratorName, + propertyName, + }, + }); + } + } + }); +} + +function isClassPropertyOptional(node: arkts.ClassProperty): boolean { + const OPTIONAL_MASK = 1 << 7; + if ((node.modifiers & OPTIONAL_MASK) !== 0) { + return true; + } else { + return false; + } +} + +const rule: UISyntaxRule = { + name: 'struct-property-optional', + messages: { + propertyOptional: `The '{{decoratorName}}' property '{{propertyName}}' cannot be an optional parameter.`, + }, + setup(context) { + return { + parsed: (node): void => { + // Check if it's a class property + if (!arkts.isClassProperty(node)) { + return; + } + if (!node.key || !arkts.isIdentifier(node.key)) { + return; + } + const keyname = node.key.name; + // If the property is optional, check the decorator further + if (!isClassPropertyOptional(node)) { + return; + } + hasPropOrRequireDecorator(context, node, keyname); + }, + }; + }, +}; + +export default rule; \ No newline at end of file diff --git a/arkui-plugins/ui-syntax-plugins/rules/validate-build-in-struct.ts b/arkui-plugins/ui-syntax-plugins/rules/validate-build-in-struct.ts index f5d308652..cb6ac9851 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/validate-build-in-struct.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/validate-build-in-struct.ts @@ -14,11 +14,10 @@ */ import * as arkts from '@koalaui/libarkts'; -import { getIdentifierName } from '../utils'; +import { getIdentifierName, BUILD_NAME } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; const NOT_PARAM_LENGTH: number = 0; -const BUILD_NAME: string = 'build'; const BUILD_FUNCTION_COUNT_INI: number = 0; const BUILD_FUNCTION_COUNT: number = 1; const NOT_STATEMENT_LENGTH: number = 0; @@ -66,7 +65,7 @@ function validateConstructorForBuildFunction( } const statements = blockStatement.statements; const structName = node.definition.ident; - if (buildFunctionCount !== BUILD_FUNCTION_COUNT && + if (buildFunctionCount === BUILD_FUNCTION_COUNT_INI && statements.length === NOT_STATEMENT_LENGTH) { reportMissingBuildInStruct(structName, blockStatement, context); } @@ -91,12 +90,43 @@ function reportMissingBuildInStruct( const endPosition = startPosition; return { range: [startPosition, endPosition], - code: '{\nbuild {\n}' + code: '{\nbuild() {\n}\n' }; } }); } +function validateDuplicateBuild( + buildFunctionCount: number, + member: arkts.MethodDefinition, + context: UISyntaxRuleContext +): void { + if (buildFunctionCount > BUILD_FUNCTION_COUNT) { + const buildNode = member.scriptFunction.id; + if (!buildNode) { + return; + } + if (!arkts.isIdentifier(buildNode)) { + return; + } + context.report({ + node: member, + message: rule.messages.invalidBuild, + data: { + structName: getIdentifierName(buildNode), + }, + fix: (structName) => { + const startPosition = member.startPosition; + const endPosition = member.endPosition; + return { + range: [startPosition, endPosition], + code: `` + }; + } + }); + } +} + function validateBuild( node: arkts.StructDeclaration, buildFunctionCount: number, @@ -107,6 +137,7 @@ function validateBuild( if (arkts.isMethodDefinition(member) && arkts.isIdentifier(member.name) && getIdentifierName(member.name) === BUILD_NAME) { buildFunctionCount++; validateBuildFunctionParameters(member, context); + validateDuplicateBuild(buildFunctionCount, member, context); } // rule2: This rule validates the use of the 'build' function if (arkts.isMethodDefinition(member) && diff --git a/arkui-plugins/ui-syntax-plugins/rules/validate-decorator-target.ts b/arkui-plugins/ui-syntax-plugins/rules/validate-decorator-target.ts new file mode 100644 index 000000000..b6b26e44f --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/validate-decorator-target.ts @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { PresetDecorators } from '../utils'; +import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; + +// Can only be used with decorators for struct +const structOnlyDecorators = [ + PresetDecorators.REUSABLE_V1, + PresetDecorators.REUSABLE_V2, + PresetDecorators.COMPONENT_V1, + PresetDecorators.COMPONENT_V2, + PresetDecorators.ENTRY, + PresetDecorators.PREVIEW, + PresetDecorators.CUSTOM_DIALOG, +]; + +// Can only be used with decorators for proerty +const proertyOnlyDecorators = [ + PresetDecorators.STATE, + PresetDecorators.PROP, + PresetDecorators.LINK, + PresetDecorators.PROVIDE, + PresetDecorators.CONSUME, + PresetDecorators.STORAGE_LINK, + PresetDecorators.STORAGE_PROP, + PresetDecorators.LOCAL_STORAGE_LINK, + PresetDecorators.LOCAL_STORAGE_PROP, + PresetDecorators.LOCAL, + PresetDecorators.PARAM, + PresetDecorators.EVENT, + PresetDecorators.PROVIDER, + PresetDecorators.CONSUMER, + PresetDecorators.WATCH, + PresetDecorators.REQUIRE, + PresetDecorators.OBJECT_LINK, + PresetDecorators.TRACK, + PresetDecorators.ONCE +]; + +// decorator check function +function validateDecorator( + annotation: arkts.AnnotationUsage, + decorator: string[], + message: string, + context: UISyntaxRuleContext, +): void { + if (annotation.expr && arkts.isIdentifier(annotation.expr)) { + if (decorator.includes(annotation.expr.name)) { + context.report({ + node: annotation, + message: message, + data: { + decoratorName: annotation.expr.name, + }, + }); + } + } +} + +function validateDecoraterStructOnly(node: arkts.AstNode, context: UISyntaxRuleContext): void { + // class + if (arkts.isClassDeclaration(node)) { + node.definition?.annotations?.forEach((annotation) => { + validateDecorator(annotation, structOnlyDecorators, rule.messages.decoratorOnlyWithStruct, context); + }); + } + // function/ variable/ method/ classproperty/ interface/ type alias declaration + if (arkts.isFunctionDeclaration(node) || + arkts.isVariableDeclaration(node) || + arkts.isClassProperty(node) || + arkts.isScriptFunction(node) || + arkts.isTSInterfaceDeclaration(node) || + arkts.isTSTypeAliasDeclaration(node) + ) { + node.annotations.forEach((annotation) => { + validateDecorator(annotation, structOnlyDecorators, rule.messages.decoratorOnlyWithStruct, context); + }); + } + + // get /set method + if (arkts.isMethodDefinition(node) && + (node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET || + node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET)) { + if (arkts.isScriptFunction(node)) { + node.annotations.forEach((annotation) => { + validateDecorator(annotation, structOnlyDecorators, rule.messages.decoratorOnlyWithStruct, context); + }); + } + } +} + +function validateDecoratorOnMethod( + node: arkts.MethodDefinition, + context: UISyntaxRuleContext, +): void { + if (arkts.isScriptFunction(node.scriptFunction)) { + node.scriptFunction.annotations?.forEach((annotation) => { + validateDecorator(annotation, proertyOnlyDecorators, rule.messages.decoratorOnlyWithMemberProerty, context); + }); + } +} + +const rule: UISyntaxRule = { + name: 'validate-decorator-target', + messages: { + decoratorOnlyWithStruct: `The '@{{decoratorName}}' decorator can only be used with 'struct'.`, + decoratorOnlyWithMemberProerty: `'@{{decoratorName}}' can only decorate member property.` + }, + setup(context) { + return { + parsed: (node): void => { + if (arkts.isMethodDefinition(node)) { + validateDecoratorOnMethod(node, context); + } + if (!arkts.isStructDeclaration(node)) { + validateDecoraterStructOnly(node, context); + } + }, + }; + }, +}; + +export default rule; \ No newline at end of file -- Gitee