diff --git a/arkui-plugins/ui-syntax-plugins/rules/attribute-no-invoke.ts b/arkui-plugins/ui-syntax-plugins/rules/attribute-no-invoke.ts new file mode 100644 index 0000000000000000000000000000000000000000..86691ae22a5deade4209d12da3988fefb9082b8f --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/attribute-no-invoke.ts @@ -0,0 +1,82 @@ +/* + * 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 { getIdentifierName, BUILD_NAME } from '../utils'; + +function attributeNoInvoke(node: arkts.AstNode, context: UISyntaxRuleContext): void { + const childNode = node.getChildren(); + if (!Array.isArray(childNode) || childNode.length < 1) { + return; + } + if (arkts.isMemberExpression(childNode[0]) && arkts.isIdentifier(childNode[0].property)) { + context.report({ + node, + message: rule.messages.cannotInitializePrivateVariables, + data: { + componentNode: node.dumpSrc(), + }, + }); + } +} + +function isInBuild(node: arkts.AstNode): boolean { + let structNode = node.parent; + while (!arkts.isMethodDefinition(structNode) || getIdentifierName(structNode.name) !== BUILD_NAME) { + if (!structNode.parent) { + return false; + } + structNode = structNode.parent; + } + return true; +} + +function chainJudgment(node: arkts.AstNode): boolean { + let childNode = node.getChildren(); + while (true) { + if (!childNode || childNode.length === 0) { + return false; + } + const firstChild = childNode[0]; + if (arkts.isIdentifier(firstChild)) { + break; + } + if (!arkts.isMemberExpression(firstChild) && !arkts.isCallExpression(firstChild)) { + return false; + } + childNode = firstChild.getChildren(); + } + return true; +} + + +const rule: UISyntaxRule = { + name: 'attribute-no-invoke', + messages: { + cannotInitializePrivateVariables: `'{{componentNode}}' does not meet UI component syntax.`, + }, + setup(context) { + return { + parsed: (node): void => { + if (arkts.isExpressionStatement(node) && isInBuild(node) && chainJudgment(node)) { + attributeNoInvoke(node, context); + } + }, + }; + }, +}; + +export default rule; \ No newline at end of file diff --git a/arkui-plugins/ui-syntax-plugins/rules/builderparam-decorator-check.ts b/arkui-plugins/ui-syntax-plugins/rules/builderparam-decorator-check.ts new file mode 100644 index 0000000000000000000000000000000000000000..67cfb950c7bfd7f75cf16e5004f4dab49199f8b8 --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/builderparam-decorator-check.ts @@ -0,0 +1,130 @@ +/* + * 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 { getIdentifierName, PresetDecorators, BUILD_NAME } from '../utils'; +import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; + +function getStructNameWithMultiplyBuilderParam( + context: UISyntaxRuleContext, + node: arkts.AstNode, + structNameWithMultiplyBuilderParam: string[], +): void { + if (arkts.nodeType(node) !== arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_MODULE) { + return; + } + node.getChildren().forEach((member) => { + if (!arkts.isStructDeclaration(member) || !member.definition.ident) { + return; + } + let count: number = 0; + let structName: string = member.definition.ident?.name ?? ''; + member.definition?.body?.forEach((item) => { + if (!arkts.isClassProperty(item) || !item.key) { + return; + } + const hasBuilderParam = item.annotations.find(annotation => + annotation.expr && arkts.isIdentifier(annotation.expr) && + annotation.expr.name === PresetDecorators.BUILDER_PARAM + ); + + if (hasBuilderParam) { + count++; + } + }); + if (count > 1) { + structNameWithMultiplyBuilderParam.push(structName); + } + }); +} + +function isInBuild(node: arkts.AstNode): boolean { + let structNode = node.parent; + arkts.isMethodDefinition(structNode); + while (!arkts.isMethodDefinition(structNode) || getIdentifierName(structNode.name) !== BUILD_NAME) { + if (!structNode.parent) { + return false; + } + structNode = structNode.parent; + } + return true; +} + +function hasBlockStatement(node: arkts.AstNode): boolean { + let parentNode = node.parent; + const siblings = parentNode.getChildren(); + if (!Array.isArray(siblings) || siblings.length < 2) { + return false; + } else if (arkts.isStringLiteral(siblings[1]) && arkts.isBlockStatement(siblings[2])) { + return true; + } else if (arkts.isBlockStatement(siblings[1])) { + return true; + } + return false; +} + +function checkComponentInitialize( + node: arkts.AstNode, + context: UISyntaxRuleContext, + structNameWithMultiplyBuilderParam: string[], +): void { + if (!arkts.isIdentifier(node) || !structNameWithMultiplyBuilderParam.includes(getIdentifierName(node))) { + return; + } + if (!hasBlockStatement(node)) { + return; + } + let structName: string = getIdentifierName(node); + let parentNode: arkts.AstNode = node.parent; + if (!arkts.isCallExpression(parentNode)) { + return; + } + let structNode = node.parent; + while (!arkts.isStructDeclaration(structNode)) { + if (!structNode.parent) { + return; + } + structNode = structNode.parent; + } + if (!isInBuild(node)) { + return; + } + context.report({ + node: node, + message: rule.messages.onlyOneBuilderParamProperty, + data: { structName }, + }); +} + + + +const rule: UISyntaxRule = { + name: 'builderparam-decorator-check', + messages: { + onlyOneBuilderParamProperty: `In the trailing lambda case, '{{structName}}' must have one and only one property decorated with @BuilderParam, and its @BuilderParam expects no parameter.`, + }, + setup(context) { + let structNameWithMultiplyBuilderParam: string[] = []; + + return { + parsed: (node): void => { + getStructNameWithMultiplyBuilderParam(context, node, structNameWithMultiplyBuilderParam,); + checkComponentInitialize(node, context, structNameWithMultiplyBuilderParam); + }, + }; + }, +}; + +export default rule; diff --git a/arkui-plugins/ui-syntax-plugins/rules/construct-parameter-literal.ts b/arkui-plugins/ui-syntax-plugins/rules/construct-parameter-literal.ts index f893b01ded67683f6d0a0e31f23845cc6a8ac9aa..6d5b75bee215f50f1e74c3c0c1404924e8de1f28 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/construct-parameter-literal.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/construct-parameter-literal.ts @@ -15,14 +15,17 @@ import * as arkts from '@koalaui/libarkts'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; -import { PresetDecorators } from '../utils'; +import { getIdentifierName, PresetDecorators } from '../utils'; function recordStructWithLinkDecorators(item: arkts.AstNode, structName: string, linkMap: Map): void { if (!arkts.isClassProperty(item)) { return; } item.annotations?.forEach((annotation) => { - const annotationName: string = annotation.expr?.dumpSrc() ?? ''; + if (!annotation.expr) { + return; + } + const annotationName: string = getIdentifierName(annotation.expr); if (annotationName === '') { return; } @@ -39,10 +42,13 @@ function initMap(node: arkts.AstNode, linkMap: Map): void { return; } node.getChildren().forEach((member) => { - if (!arkts.isStructDeclaration(member)) { + if (!(arkts.isStructDeclaration(member))) { + return; + } + if (!member.definition || !member.definition.ident || !arkts.isIdentifier(member.definition.ident)) { return; } - const structName: string = member.definition.ident?.name ?? ''; + const structName: string = member.definition.ident.name; if (structName === '') { return; } @@ -55,40 +61,41 @@ function initMap(node: arkts.AstNode, linkMap: Map): void { function checkInitializeWithLiteral(node: arkts.AstNode, context: UISyntaxRuleContext, linkMap: Map ): void { - if (!arkts.isCallExpression(node)) { + if (!arkts.isCallExpression(node) || !arkts.isIdentifier(node.expression)) { return; } - const componentName = node.expression.dumpSrc(); + const componentName = node.expression.name; // Only assignments to properties decorated with Link or ObjectLink trigger rule checks if (!linkMap.has(componentName)) { return; } node.arguments.forEach((member) => { member.getChildren().forEach((property) => { - if (!arkts.isProperty(property)) { + if (!arkts.isProperty(property) || !property.key || !property.value) { return; } - if (property.value === undefined) { + const key: string = getIdentifierName(property.key); + if (key === '') { return; } - const propertyType: arkts.Es2pandaAstNodeType = arkts.nodeType(property.value); - const key: string = property.key?.dumpSrc() ?? ''; - if (key === '') { + // If the assignment statement is of type MemberExpression or Identifier, it is not judged + if (arkts.isMemberExpression(property.value) && arkts.isThisExpression(property.value.object)) { return; } - const value = property.value?.dumpSrc() ? property.value.dumpSrc() : ''; - // If the assignment statement is not of type MemberExpression, throw an error - if (propertyType !== arkts.Es2pandaAstNodeType.AST_NODE_TYPE_MEMBER_EXPRESSION) { - context.report({ - node: property, - message: rule.messages.cannotInitializeWithLiteral, - data: { - value: value, - annotationName: linkMap.get(componentName)!, - key: key, - }, - }); + if (arkts.isIdentifier(property.value)) { + return; } + const initializerName = property.value.dumpSrc().replace(/\(this\)/g, 'this'); + const parameter: string = linkMap.get(componentName)!; + context.report({ + node: property, + message: rule.messages.initializerIsLiteral, + data: { + initializerName: initializerName, + parameter: `@${parameter}`, + parameterName: key, + }, + }); }); }); } @@ -96,7 +103,7 @@ function checkInitializeWithLiteral(node: arkts.AstNode, context: UISyntaxRuleCo const rule: UISyntaxRule = { name: 'construct-parameter-literal', messages: { - cannotInitializeWithLiteral: `Assigning the attribute'{{value}}' to the '@{{annotationName}}' decorated attribute '{{key}}' is not allowed.`, + initializerIsLiteral: `The 'regular' property '{{initializerName}}' cannot be assigned to the '{{parameter}}' property '{{parameterName}}'.`, }, setup(context) { let linkMap: Map = new Map(); diff --git a/arkui-plugins/ui-syntax-plugins/rules/construct-parameter.ts b/arkui-plugins/ui-syntax-plugins/rules/construct-parameter.ts new file mode 100644 index 0000000000000000000000000000000000000000..5171f77ebb8476c968e60b73b72cd00c833f2783 --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/construct-parameter.ts @@ -0,0 +1,319 @@ +/* + * 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 { + getIdentifierName, + getClassPropertyName, + getClassPropertyAnnotationNames, + PresetDecorators, + getAnnotationName, +} from '../utils'; +import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; + +// When a specific decorator is used as a parameter, the assigned decorator is not allowed +const disallowAssignedDecorators: string[] = [ + PresetDecorators.REGULAR, PresetDecorators.LINK, PresetDecorators.OBJECT_LINK, + PresetDecorators.BUILDER_PARAM, PresetDecorators.LOCAL_BUILDER, PresetDecorators.STATE, + PresetDecorators.PROP, PresetDecorators.PROVIDE, PresetDecorators.CONSUME, + PresetDecorators.BUILDER, +]; +// The decorator structure prohibits initializing the assignment list +const restrictedDecoratorInitializations: Map = new Map([ + [PresetDecorators.REGULAR, [PresetDecorators.OBJECT_LINK, PresetDecorators.LINK]], + [PresetDecorators.PROVIDE, [PresetDecorators.REGULAR]], + [PresetDecorators.CONSUME, [PresetDecorators.REGULAR]], + [PresetDecorators.STORAGE_PROP, [PresetDecorators.REGULAR]], + [PresetDecorators.VARIABLE, [PresetDecorators.LINK]], + [PresetDecorators.LOCAL_STORAGE_LINK, [PresetDecorators.REGULAR]], + [PresetDecorators.LOCAL_STORAGE_PROP, [PresetDecorators.REGULAR]], +]); +// When there are multiple Decorators, filter out the Decorators that are not relevant to the rule +const decoratorsFilter: string[] = [ + PresetDecorators.PROVIDE, PresetDecorators.CONSUME, PresetDecorators.STORAGE_PROP, + PresetDecorators.LOCAL_STORAGE_LINK, PresetDecorators.LOCAL_STORAGE_PROP, PresetDecorators.BUILDER_PARAM, +]; + +function getPropertyAnnotationName(node: arkts.AstNode, propertyName: string): string { + while (!arkts.isStructDeclaration(node)) { + node = node.parent; + } + let annotationNames: string[] = []; + node.definition.body.forEach((item) => { + if (arkts.isClassProperty(item) && getClassPropertyName(item) === propertyName) { + annotationNames = getClassPropertyAnnotationNames(item); + } + if (arkts.isMethodDefinition(item) && getIdentifierName(item.name) === propertyName) { + annotationNames = item.scriptFunction.annotations.map((annotation) => + getAnnotationName(annotation) + ); + } + }); + if (annotationNames.length === 0) { + return PresetDecorators.REGULAR; + } + const annotationName = annotationNames.find((item) => { return decoratorsFilter.includes(item) }); + if (annotationName) { + return annotationName; + } + return ''; +} + +// Define a function to add property data to the property map +function addProperty( + structName: string, + propertyName: string, + annotationName: string, + propertyMap: Map> +): void { + if (!propertyMap.has(structName)) { + propertyMap.set(structName, new Map()); + } + const structProperties = propertyMap.get(structName); + if (!structProperties) { + return; + } + structProperties.set(propertyName, annotationName); +} + +function collectBuilderFunctions(member: arkts.AstNode, builderFunctionList: string[]): void { + if (!arkts.isFunctionDeclaration(member) || !member.annotations) { + return; + } + member.annotations.forEach(annotation => { + if (annotation.expr && getIdentifierName(annotation.expr) === PresetDecorators.BUILDER && + member.scriptFunction.id) { + builderFunctionList.push(member.scriptFunction.id.name); + } + }); +} + +function collectRegularVariables(member: arkts.AstNode, regularVariableList: string[]): void { + if (!arkts.isVariableDeclaration(member) || !member.declarators) { + return; + } + member.getChildren().forEach((item) => { + if (!arkts.isVariableDeclarator(item) || !item.name || + (item.initializer && arkts.isArrowFunctionExpression(item.initializer))) { + return; + } + regularVariableList.push(item.name.name); + }); +} + +function initList(node: arkts.AstNode, regularVariableList: string[], builderFunctionList: string[]): void { + // Record variables and functions that @builder decorate + if (arkts.nodeType(node) !== arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_MODULE) { + return; + } + node.getChildren().forEach((member) => { + collectBuilderFunctions(member, builderFunctionList); + collectRegularVariables(member, regularVariableList); + }); +} + +function recordRestrictedDecorators( + item: arkts.AstNode, + structName: string, + propertyMap: Map> +): void { + if (!arkts.isClassProperty(item) || !item.key) { + return; + } + let propertyName: string = getIdentifierName(item.key); + // If there is no decorator, it is a regular type + if (item.annotations.length === 0) { + let annotationName: string = PresetDecorators.REGULAR; + addProperty(structName, propertyName, annotationName, propertyMap); + } + // Iterate through the decorator of the property, and when the decorator is in the disallowAssignedDecorators array, the property is recorded + item.annotations.forEach((annotation) => { + if (!annotation.expr) { + return; + } + let annotationName: string = getIdentifierName(annotation.expr); + if (disallowAssignedDecorators.includes(annotationName)) { + addProperty(structName, propertyName, annotationName, propertyMap); + } + }); +} + +function initPropertyMap(node: arkts.AstNode, propertyMap: Map>): void { + // Iterate through the root node ahead of time, noting the structure name, variable name, and corresponding decorator type + if (arkts.nodeType(node) !== arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_MODULE) { + return; + } + node.getChildren().forEach((member) => { + if (!arkts.isStructDeclaration(member) || !member.definition.ident) { + return; + } + let structName: string = member.definition.ident?.name ?? ''; + if (structName === '') { + return; + } + member.definition?.body?.forEach((item) => { + recordRestrictedDecorators(item, structName, propertyMap); + }); + }); +} + +function reportRegularVariableError( + property: arkts.AstNode, + context: UISyntaxRuleContext, + childType: string, + childName: string, + regularVariableList: string[] +): void { + if (!arkts.isProperty(property) || !property.value) { + return; + } + if (childType !== PresetDecorators.LINK) { + return; + } + if (arkts.isIdentifier(property.value) && regularVariableList.includes(property.value.name)) { + context.report({ + node: property, + message: rule.messages.constructParameter, + data: { + initializer: PresetDecorators.REGULAR, + initializerName: property.value.name, + parameter: `@${childType}`, + parameterName: childName, + }, + }); + } +} + +function reportBuilderError( + property: arkts.AstNode, + context: UISyntaxRuleContext, + childType: string, + childName: string, + builderFunctionList: string[], +): void { + if (!arkts.isProperty(property) || !property.value) { + return; + } + let isLocalBuilder: boolean = false; + if (arkts.isMemberExpression(property.value) && arkts.isIdentifier(property.value.property)) { + const parentName = property.value.property.name; + const parentType: string = getPropertyAnnotationName(property, parentName); + if (parentType === '') { + return; + } + isLocalBuilder = parentType === PresetDecorators.LOCAL_BUILDER; + } + let isBuilder: boolean = false; + if (arkts.isIdentifier(property.value)) { + isBuilder = builderFunctionList.includes(property.value.name); + if (builderFunctionList.includes(property.value.name) && childType !== PresetDecorators.BUILDER_PARAM) { + context.report({ + node: property, + message: rule.messages.initializerIsBuilder, + data: { + initializerName: property.value.name, + parameterName: childName, + }, + }); + } + } + if (childType === PresetDecorators.BUILDER_PARAM && !isBuilder && !isLocalBuilder) { + context.report({ + node: property, + message: rule.messages.parameterIsBuilderParam, + data: { + parameterName: childName, + }, + }); + } +} + +function checkConstructParameter( + node: arkts.AstNode, + context: UISyntaxRuleContext, + propertyMap: Map>, + regularVariableList: string[], + builderFunctionList: string[], +): void { + if (!arkts.isIdentifier(node) || !propertyMap.has(getIdentifierName(node))) { + return; + } + let structName: string = getIdentifierName(node); + let parentNode: arkts.AstNode = node.parent; + if (!arkts.isCallExpression(parentNode)) { + return; + } + // Gets all the properties recorded by the struct + const childPropertyName: Map = propertyMap.get(structName)!; + parentNode.arguments.forEach((member) => { + member.getChildren().forEach((property) => { + if (!arkts.isProperty(property) || !property.key) { + return; + } + const childName = getIdentifierName(property.key); + if (!childPropertyName.has(childName) || !property.value) { + return; + } + const childType: string = childPropertyName.get(childName)!; + reportRegularVariableError(property, context, childType, childName, regularVariableList); + reportBuilderError(property, context, childType, childName, builderFunctionList); + if (!arkts.isMemberExpression(property.value) || !arkts.isThisExpression(property.value.object)) { + return; + } + const parentName = getIdentifierName(property.value.property); + const parentType: string = getPropertyAnnotationName(node, parentName); + if (parentType === '') { + return; + } + if (restrictedDecoratorInitializations.has(parentType) && + restrictedDecoratorInitializations.get(parentType)!.includes(childType)) { + context.report({ + node: property, + message: rule.messages.constructParameter, + data: { + initializer: parentType === PresetDecorators.REGULAR ? PresetDecorators.REGULAR : `@${parentType}`, + initializerName: parentName, + parameter: childType === PresetDecorators.REGULAR ? PresetDecorators.REGULAR : `@${childType}`, + parameterName: childName, + }, + }); + } + }); + }); +} + +const rule: UISyntaxRule = { + name: 'construct-parameter', + messages: { + constructParameter: `The '{{initializer}}' property '{{initializerName}}' cannot be assigned to the '{{parameter}}' property '{{parameterName}}'.`, + initializerIsBuilder: `'@Builder' function '{{initializerName}}' can only initialize '@BuilderParam' attribute.`, + parameterIsBuilderParam: `'@BuilderParam' attribute '{{parameterName}}' can only initialized by '@Builder' function or '@LocalBuilder' method in struct.`, + }, + setup(context) { + let propertyMap: Map> = new Map(); + let regularVariableList: string[] = []; + let builderFunctionList: string[] = []; + + return { + parsed: (node): void => { + initList(node, regularVariableList, builderFunctionList); + initPropertyMap(node, propertyMap); + checkConstructParameter(node, context, propertyMap, regularVariableList, builderFunctionList); + }, + }; + }, +}; + +export default rule; \ No newline at end of file diff --git a/arkui-plugins/ui-syntax-plugins/rules/entry-struct-no-export.ts b/arkui-plugins/ui-syntax-plugins/rules/entry-struct-no-export.ts index 7017ab54f4e3b08dfb6f28480dd1e747746aa1af..dfeee1b319d5299d8872d8db4293a309d0cf6c2a 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/entry-struct-no-export.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/entry-struct-no-export.ts @@ -20,7 +20,7 @@ import { UISyntaxRule } from './ui-syntax-rule'; const rule: UISyntaxRule = { name: 'entry-struct-no-export', messages: { - noExportWithEntry: `It's not a recommended way to export struct with @Entry decorator, which may cause ACE Engine error in component preview mode.`, + noExportWithEntry: `It's not a recommended way to export struct with '@Entry' decorator, which may cause ACE Engine error in component preview mode.`, }, setup(context) { return { @@ -36,10 +36,11 @@ const rule: UISyntaxRule = { ); //Determines whether the struct is exported - const isExported = node.dumpSrc().includes('export struct'); + const isStructExport = node.isExport; + const isStructDefaultExport = node.isDefaultExport; // If a @Entry decorator is present and the struct is exported - if (entryDecoratorUsage && isExported) { + if (entryDecoratorUsage && (isStructExport || isStructDefaultExport)) { context.report({ node: entryDecoratorUsage, message: this.messages.noExportWithEntry, diff --git a/arkui-plugins/ui-syntax-plugins/rules/index.ts b/arkui-plugins/ui-syntax-plugins/rules/index.ts index 13b008b627bb821cbb05603a977ad1238898fc87..62e8288dfbec77a8a00a2ecdc1e406e62a126e5d 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/index.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/index.ts @@ -14,14 +14,17 @@ */ import { UISyntaxRule } from './ui-syntax-rule'; +import AttributeNoInvoke from './attribute-no-invoke'; +import BuilderparamDecoratorCheck from './builderparam-decorator-check.ts'; import BuildRootNode from './build-root-node'; import CheckConstructPrivateParameter from './check-construct-private-parameter'; import CheckDecoratedPropertyType from './check-decorated-property-type'; import ComponentComponentV2MixUseCheck from './component-componentV2-mix-use-check'; import ComponentV2MixCheck from './componentV2-mix-check'; +import ConstructParameterLiteral from './construct-parameter-literal'; +import ConstructParameter from './construct-parameter'; import ConsumerProviderDecoratorCheck from './consumer-provider-decorator-check'; import ComponentV2StateUsageValidation from './componentV2-state-usage-validation'; -import ConstructParameterLiteral from './construct-parameter-literal'; import CustomDialogMissingController from './custom-dialog-missing-controller'; import DecoratorsInUIComponentOnly from './decorators-in-ui-component-only'; import EntryLoacalStorageCheck from './entry-localstorage-check'; @@ -32,6 +35,7 @@ import NestedRelationship from './nested-relationship'; import NoChildInButton from './no-child-in-button'; import NoDuplicateDecorators from './no-duplicate-decorators'; import NoDuplicateEntry from './no-duplicate-entry'; +import NoDuplicateId from './no-duplicate-id'; import NoDuplicatePreview from './no-duplicate-preview'; import NoDuplicateStateManager from './no-duplicate-state-manager'; import NoPropLinkObjectlinkInEntry from './no-prop-link-objectlink-in-entry'; @@ -55,14 +59,17 @@ import OneDecoratorOnFunctionMethod from './one-decorator-on-function-method'; import OldNewDecoratorMixUseCheck from './old-new-decorator-mix-use-check'; const rules: UISyntaxRule[] = [ + AttributeNoInvoke, + BuilderparamDecoratorCheck, BuildRootNode, CheckConstructPrivateParameter, CheckDecoratedPropertyType, ComponentComponentV2MixUseCheck, ComponentV2MixCheck, + ConstructParameterLiteral, + ConstructParameter, ConsumerProviderDecoratorCheck, ComponentV2StateUsageValidation, - ConstructParameterLiteral, CustomDialogMissingController, DecoratorsInUIComponentOnly, EntryLoacalStorageCheck, @@ -73,6 +80,7 @@ const rules: UISyntaxRule[] = [ NoChildInButton, NoDuplicateDecorators, NoDuplicateEntry, + NoDuplicateId, NoDuplicatePreview, NoDuplicateStateManager, NoPropLinkObjectlinkInEntry, diff --git a/arkui-plugins/ui-syntax-plugins/rules/no-duplicate-id.ts b/arkui-plugins/ui-syntax-plugins/rules/no-duplicate-id.ts new file mode 100644 index 0000000000000000000000000000000000000000..bca4ccdf6dd5e3dd1fd55259298fcf5678a9bc51 --- /dev/null +++ b/arkui-plugins/ui-syntax-plugins/rules/no-duplicate-id.ts @@ -0,0 +1,129 @@ +/* + * 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 { getIdentifierName } from '../utils'; +import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; + +interface IdInfo { + value: string; + node: arkts.AstNode; +} + +const ID_NAME: string = 'id'; + +function getIdValue(expression: arkts.CallExpression): string | undefined { + let value: string | undefined; + for (const argument of expression.arguments) { + if (arkts.isStringLiteral(argument)) { + value = argument.str; + break; + } + } + return value; +} + +function getIdInfo(expression: arkts.CallExpression): IdInfo | undefined { + + if (!arkts.isMemberExpression(expression.expression)) { + return undefined; + } + const member = expression.expression; + if (!arkts.isIdentifier(member.property)) { + return undefined; + } + const propertyName = getIdentifierName(member.property); + if (propertyName !== ID_NAME) { + return undefined; + } + let value: string | undefined = getIdValue(expression); + if (!value) { + return undefined; + } + const currentIdInfo: IdInfo = { + value, + node: member.property + }; + return currentIdInfo; +} + +function validateDuplicateId( + currentIdInfo: IdInfo, + usedIds: Map, + context: UISyntaxRuleContext, +): void { + + if (usedIds.has(currentIdInfo.value)) { + context.report({ + node: currentIdInfo.node, + message: rule.messages.duplicateId, + data: { + id: currentIdInfo.value, + path: getPath() ?? '', + line: currentIdInfo.node.startPosition.line().toString(), + index: currentIdInfo.node.startPosition.index().toString() + } + }); + } else { + // Otherwise, record it + usedIds.set(currentIdInfo.value, currentIdInfo); + } +} + +function getPath(): string | undefined { + const contextPtr = arkts.arktsGlobal.compilerContext?.peer; + if (!!contextPtr) { + let program = arkts.getOrUpdateGlobalContext(contextPtr).program; + return program.programGlobalAbsName; + } + return undefined; +} + +function findAndValidateIds( + node: arkts.BlockStatement, + usedIds: Map, + context: UISyntaxRuleContext +): void { + node.statements.forEach((statement) => { + if ( + arkts.isExpressionStatement(statement) && + arkts.isCallExpression(statement.expression) + ) { + const idInfo = getIdInfo(statement.expression); + if (idInfo) { + validateDuplicateId(idInfo, usedIds, context); + } + } + }); +} + +const rule: UISyntaxRule = { + name: 'no-duplicate-id', + messages: { + duplicateId: `The current component id "{{id}}" is duplicate with {{path}}:{{line}}:{{index}}.`, + }, + setup(context) { + const usedIds = new Map(); + return { + parsed(node): void { + if (arkts.isBlockStatement(node)) { + findAndValidateIds(node, usedIds, context); + } + } + }; + }, +}; + +export default rule; \ No newline at end of file diff --git a/arkui-plugins/ui-syntax-plugins/rules/one-decorator-on-function-method.ts b/arkui-plugins/ui-syntax-plugins/rules/one-decorator-on-function-method.ts index 2e9f11fbf250cb483baee025d3128a49b6bc15e3..0cddd55d0b06c726c8850766ec7f5984d790e804 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/one-decorator-on-function-method.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/one-decorator-on-function-method.ts @@ -18,32 +18,28 @@ import { getAnnotationName, PresetDecorators } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; const allowedDecorators = PresetDecorators.BUILDER; +const PARAM_THIS_NAME = '=t'; const DECORATOR_LIMIT = 1; -function validateFunctionDecorator(node: arkts.EtsScript, context: UISyntaxRuleContext): void { - node.statements.forEach((statement) => { - // If the node is not a function declaration, it is returned - if (!arkts.isFunctionDeclaration(statement)) { - return; - } - const annotations = statement.annotations; - // If there is no annotation, go straight back - if (!annotations) { - return; - } - const otherDecorator = annotations?.find(annotation => - annotation.expr && arkts.isIdentifier(annotation.expr) && - annotation.expr.name !== PresetDecorators.BUILDER - ); - // Check that each annotation is in the list of allowed decorators - annotations.forEach((annotation) => { - const decoratorName = getAnnotationName(annotation); - // rule1: misuse of decorator, only '@Builder' , '@Styles' decorators allowed on global functions - if (allowedDecorators !== decoratorName || - (allowedDecorators === decoratorName && decoratorName.length > DECORATOR_LIMIT)) { - reportInvalidDecorator(annotation, otherDecorator, context); - } - }); +function findDecorator(annotations: arkts.AnnotationUsage[], decorator: string): arkts.AnnotationUsage | undefined { + return annotations?.find(annotation => + annotation.expr && arkts.isIdentifier(annotation.expr) && + annotation.expr.name === decorator + ); +} + +function otherDecoratorFilter(annotations: arkts.AnnotationUsage[]): arkts.AnnotationUsage | undefined { + return annotations?.find(annotation => + annotation.expr && arkts.isIdentifier(annotation.expr) && + annotation.expr.name !== PresetDecorators.BUILDER + ); +} + +function hasThisParameter(member: arkts.ScriptFunction): boolean { + return member.params.some((param) => { + return arkts.isEtsParameterExpression(param) && + arkts.isIdentifier(param.identifier) && + param.identifier.name === PARAM_THIS_NAME; }); } @@ -69,6 +65,45 @@ function reportInvalidDecorator( }); } +function validateAllowedDecorators( + annotations: arkts.AnnotationUsage[], + otherDecorator: arkts.AnnotationUsage | undefined, + context: UISyntaxRuleContext +): void { + annotations.forEach((annotation) => { + const decoratorName = getAnnotationName(annotation); + // rule1: misuse of decorator, only '@Builder' decorator allowed on global functions + if (allowedDecorators !== decoratorName || + (allowedDecorators === decoratorName && decoratorName.length > DECORATOR_LIMIT)) { + reportInvalidDecorator(annotation, otherDecorator, context); + } + }); +} + +function validateFunctionDecorator(node: arkts.EtsScript, context: UISyntaxRuleContext): void { + node.statements.forEach((statement) => { + // If the node is not a function declaration, it is returned + if (!arkts.isFunctionDeclaration(statement)) { + return; + } + const annotations = statement.annotations; + // If there is no annotation, go straight back + if (!annotations) { + return; + } + // @AnimatableExtend decorators can only be used with functions with this parameter. + const animatableExtendDecorator = findDecorator(annotations, PresetDecorators.ANIMATABLE_EXTEND); + if (arkts.isScriptFunction(statement.scriptFunction) && animatableExtendDecorator) { + const member = statement.scriptFunction; + if (hasThisParameter(member)) { + return; + } + } + // Check that each annotation is in the list of allowed decorators + validateAllowedDecorators(annotations, otherDecoratorFilter(annotations), context); + }); +} + const rule: UISyntaxRule = { name: 'one-decorator-on-function-method', messages: { diff --git a/arkui-plugins/ui-syntax-plugins/rules/watch-decorator-function.ts b/arkui-plugins/ui-syntax-plugins/rules/watch-decorator-function.ts index dae346a7958033a1d2c0dc703d2ece3c2554682c..901e208bff206903d5f449da76f2eb1fa967aff9 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/watch-decorator-function.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/watch-decorator-function.ts @@ -14,14 +14,34 @@ */ import * as arkts from '@koalaui/libarkts'; -import { getIdentifierName, PresetDecorators } from '../utils'; +import { getIdentifierName, isPrivateClassProperty, PresetDecorators, getClassPropertyName } from '../utils'; import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; +function getExpressionValue(parameters: arkts.Expression, privateNames: string[]): string { + const type = arkts.nodeType(parameters); + if (type === arkts.Es2pandaAstNodeType.AST_NODE_TYPE_NUMBER_LITERAL) { + return parameters.dumpSrc(); // Try extracting the string representation with dumpSrc + } else if (type === arkts.Es2pandaAstNodeType.AST_NODE_TYPE_BOOLEAN_LITERAL) { + return parameters.dumpSrc(); + } else if (type === arkts.Es2pandaAstNodeType.AST_NODE_TYPE_NULL_LITERAL) { + return 'null'; + } else if (type === arkts.Es2pandaAstNodeType.AST_NODE_TYPE_UNDEFINED_LITERAL) { + return 'undefined'; + } else if (arkts.isMemberExpression(parameters)) { + if (arkts.isIdentifier(parameters.property)) { + if (privateNames.includes(parameters.property.name)) { + return parameters.property.name; + } + } + } + return parameters.dumpSrc(); // By default, an empty string is returned +} + // Gets the names of all methods in the struct function getMethodNames(node: arkts.StructDeclaration): string[] { const methodNames: string[] = []; node.definition.body.forEach((member) => { - if (arkts.isMethodDefinition(member)) { + if (arkts.isMethodDefinition(member) && arkts.isIdentifier(member.name)) { const methodName = getIdentifierName(member.name); if (methodName) { methodNames.push(methodName); @@ -31,23 +51,60 @@ function getMethodNames(node: arkts.StructDeclaration): string[] { return methodNames; } +function getPrivateNames(node: arkts.StructDeclaration): string[] { + const privateNames: string[] = []; + node.definition.body.forEach((member) => { + if (arkts.isClassProperty(member) && isPrivateClassProperty(member)) { + const privateName = getClassPropertyName(member); + if (privateName) { + privateNames.push(privateName); + } + } + }); + return privateNames; +} + // Invalid @Watch decorator bugs are reported function reportInvalidWatch( member: arkts.ClassProperty, - methodName: string, + parameterName: string, hasWatchDecorator: arkts.AnnotationUsage, context: UISyntaxRuleContext ): void { context.report({ node: hasWatchDecorator, message: rule.messages.invalidWatch, - data: { methodName }, + data: { parameterName }, + fix: () => { + const startPosition = member.endPosition; + const endPosition = member.endPosition; + return { + range: [startPosition, endPosition], + code: `\n${parameterName}(){\n}`, + }; + }, + }); +} + +function reportStringOnly( + parameters: arkts.Expression | undefined, + privateNames: string[], + hasWatchDecorator: arkts.AnnotationUsage, + context: UISyntaxRuleContext +): void { + if (!parameters) { + return; + } + context.report({ + node: hasWatchDecorator, + message: rule.messages.stringOnly, + data: { parameterName: getExpressionValue(parameters, privateNames) }, fix: () => { - const startPosition = arkts.getEndPosition(member); - const endPosition = arkts.getEndPosition(member); + const startPosition = parameters.startPosition; + const endPosition = parameters.endPosition; return { range: [startPosition, endPosition], - code: `\n${methodName}(){\n}`, + code: ``, }; }, }); @@ -56,11 +113,12 @@ function reportInvalidWatch( function validateWatchDecorator( member: arkts.ClassProperty, methodNames: string[], + privateNames: string[], hasWatchDecorator: arkts.AnnotationUsage | undefined, context: UISyntaxRuleContext ): void { member.annotations.forEach((annotation) => { - validateWatchProperty(annotation, member, methodNames, hasWatchDecorator, context); + validateWatchProperty(annotation, member, methodNames, privateNames, hasWatchDecorator, context); }); } @@ -68,29 +126,43 @@ function validateWatchProperty( annotation: arkts.AnnotationUsage, member: arkts.ClassProperty, methodNames: string[], + privateNames: string[], hasWatchDecorator: arkts.AnnotationUsage | undefined, context: UISyntaxRuleContext ): void { if ( - annotation.expr && - annotation.expr.dumpSrc() === PresetDecorators.WATCH + !annotation.expr || + !arkts.isIdentifier(annotation.expr) || + annotation.expr.name !== PresetDecorators.WATCH ) { - annotation.properties.forEach((element) => { - if (!arkts.isClassProperty(element)) { + return; + } + annotation.properties.forEach((element) => { + if (!arkts.isClassProperty(element)) { + return; + } + if (!element.value) { + return; + } + if (!arkts.isStringLiteral(element.value)) { + if (!hasWatchDecorator) { return; } - const methodName = element.value?.dumpSrc().slice(1, -1); - if (hasWatchDecorator && methodName && !methodNames.includes(methodName)) { - reportInvalidWatch(member, methodName, hasWatchDecorator, context); - } - }); - } + reportStringOnly(element.value, privateNames, hasWatchDecorator, context); + return; + } + const parameterName = element.value.str; + if (hasWatchDecorator && parameterName && !methodNames.includes(parameterName)) { + reportInvalidWatch(member, parameterName, hasWatchDecorator, context); + } + }); } function validateWatch( node: arkts.StructDeclaration, methodNames: string[], + privateNames: string[], context: UISyntaxRuleContext ): void { node.definition.body.forEach(member => { @@ -98,18 +170,19 @@ function validateWatch( return; } const hasWatchDecorator = member.annotations?.find(annotation => - annotation.expr && - annotation.expr.dumpSrc() === PresetDecorators.WATCH + annotation.expr && arkts.isIdentifier(annotation.expr) && + annotation.expr.name === PresetDecorators.WATCH ); // Determine whether it contains @watch decorators - validateWatchDecorator(member, methodNames, hasWatchDecorator, context); + validateWatchDecorator(member, methodNames, privateNames, hasWatchDecorator, context); }); } const rule: UISyntaxRule = { name: 'watch-decorator-function', messages: { - invalidWatch: `The '@Watch' decorated parameter must be a callback '{{methodName}}' of a function in a custom component.`, + invalidWatch: `'@watch' cannot be used with '{{parameterName}}'. Apply it only to parameters that correspond to existing methods.`, + stringOnly: `'@Watch' cannot be used with '{{parameterName}}'. Apply it only to 'string' parameters.`, }, setup(context) { return { @@ -119,7 +192,9 @@ const rule: UISyntaxRule = { } // Get all method names const methodNames = getMethodNames(node); - validateWatch(node, methodNames, context); + // Get a private variable + const privateNames = getPrivateNames(node); + validateWatch(node, methodNames, privateNames, context); }, }; }, diff --git a/arkui-plugins/ui-syntax-plugins/utils/index.ts b/arkui-plugins/ui-syntax-plugins/utils/index.ts index df15e319799c1651a95b80e852903b04a130231d..fc45fee954710166ccf3ef006254ce7a2d71d9e7 100644 --- a/arkui-plugins/ui-syntax-plugins/utils/index.ts +++ b/arkui-plugins/ui-syntax-plugins/utils/index.ts @@ -19,6 +19,8 @@ import * as path from 'path'; import { UISyntaxRuleComponents } from 'ui-syntax-plugins/processor'; import { UISyntaxRuleContext } from 'ui-syntax-plugins/rules/ui-syntax-rule'; +export const BUILD_NAME: string = 'build'; + export const SINGLE_CHILD_COMPONENT: number = 1; export const MAX_ENTRY_DECORATOR_COUNT: number = 1; export const MAX_PREVIEW_DECORATOR_COUNT: number = 10; diff --git a/koala-wrapper/src/arkts-api/peers/AstNode.ts b/koala-wrapper/src/arkts-api/peers/AstNode.ts index ffab3c5cfaf22ad799ca1dee1a677ec16ed4e35f..54b699cd2138dc151da9c666271f34793e2149ec 100644 --- a/koala-wrapper/src/arkts-api/peers/AstNode.ts +++ b/koala-wrapper/src/arkts-api/peers/AstNode.ts @@ -126,6 +126,14 @@ export abstract class AstNode extends ArktsObject { ); } + public get isExport(): boolean { + return global.generatedEs2panda._AstNodeIsExportedConst(global.context, this.peer); + } + + public get isDefaultExport(): boolean { + return global.generatedEs2panda._AstNodeIsDefaultExportedConst(global.context, this.peer); + } + public get isStatic(): boolean { return global.generatedEs2panda._AstNodeIsStaticConst(global.context, this.peer); }