From 6b1f45e61c1ea1af8b2a47847dcdfc45319b9fa1 Mon Sep 17 00:00:00 2001 From: youzhi92 Date: Wed, 18 Jun 2025 20:41:40 +0800 Subject: [PATCH] feat: support to config the level of syntax rule Signed-off-by: youzhi92 Change-Id: Ia1730d4246b05ebc9da9174c3cba050fd1c02b76 --- .../ui-syntax-plugins/processor/index.ts | 215 ++++++++++-------- .../rules/build-root-node.ts | 164 ++++++------- .../ui-syntax-plugins/rules/index.ts | 108 ++++----- .../ui-syntax-plugins/rules/ui-syntax-rule.ts | 63 +++-- 4 files changed, 291 insertions(+), 259 deletions(-) diff --git a/arkui-plugins/ui-syntax-plugins/processor/index.ts b/arkui-plugins/ui-syntax-plugins/processor/index.ts index d9bb88a10..0a043b9f3 100644 --- a/arkui-plugins/ui-syntax-plugins/processor/index.ts +++ b/arkui-plugins/ui-syntax-plugins/processor/index.ts @@ -14,133 +14,146 @@ */ import * as arkts from '@koalaui/libarkts'; -import * as path from "node:path"; -import { ReportOptions, UISyntaxRule, UISyntaxRuleContext, UISyntaxRuleHandler } from '../rules/ui-syntax-rule'; +import * as path from 'node:path'; +import { + ReportOptions, + UISyntaxRule, + UISyntaxRuleConfig, + UISyntaxRuleContext, + UISyntaxRuleHandler, +} from '../rules/ui-syntax-rule'; import { getUIComponents, readJSON, UISyntaxRuleComponents } from '../utils'; import { ProjectConfig } from 'common/plugin-context'; export type UISyntaxRuleProcessor = { - setProjectConfig(projectConfig: ProjectConfig): void; - parsed(node: arkts.AstNode): void; + setProjectConfig(projectConfig: ProjectConfig): void; + parsed(node: arkts.AstNode): void; }; type ModuleConfig = { - module: { - pages: string; - } -} + module: { + pages: string; + }; +}; type MainPages = { - src: string[]; + src: string[]; }; -const BASE_RESOURCE_PATH = "src/main/resources/base"; -const ETS_PATH = "src/main/ets"; +const BASE_RESOURCE_PATH = 'src/main/resources/base'; +const ETS_PATH = 'src/main/ets'; class ConcreteUISyntaxRuleContext implements UISyntaxRuleContext { - public componentsInfo: UISyntaxRuleComponents; - public projectConfig?: ProjectConfig; - - constructor() { - this.componentsInfo = getUIComponents('../../components/'); - } - - public report(options: ReportOptions): void { - let message: string; - if (!options.data) { - message = options.message; - } else { - message = this.format(options.message, options.data); - } + public componentsInfo: UISyntaxRuleComponents; + public projectConfig?: ProjectConfig; - const kind: arkts.DiagnosticKind = - arkts.DiagnosticKind.create(message, arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_ERROR); - if (options.fix) { - const diagnosticInfo: arkts.DiagnosticInfo = arkts.DiagnosticInfo.create(kind); - const fixSuggestion = options.fix(options.node); - const suggestionInfo: arkts.SuggestionInfo = arkts.SuggestionInfo.create(kind, fixSuggestion.code); - const [startPosition, endPosition] = fixSuggestion.range; - const sourceRange: arkts.SourceRange = arkts.SourceRange.create(startPosition, endPosition); - arkts.Diagnostic.logDiagnosticWithSuggestion(diagnosticInfo, suggestionInfo, sourceRange); - } else { - arkts.Diagnostic.logDiagnostic(kind, arkts.getStartPosition(options.node)); + constructor() { + this.componentsInfo = getUIComponents('../../components/'); } - // todo - const position = arkts.getStartPosition(options.node); - if (options.fix) { - const suggestion = options.fix(options.node); - console.log(`syntax-error: ${message}`); - console.log(`range: (${suggestion.range[0].index()}, ${suggestion.range[0].line()}) - (${suggestion.range[1].index()}, ${suggestion.range[1].line()})`, - `code: ${suggestion.code}`); - } else { - console.log(`syntax-error: ${message} (${position.index()},${position.line()})`); + public report(options: ReportOptions): void { + let message: string; + if (!options.data) { + message = options.message; + } else { + message = this.format(options.message, options.data); + } + + const kind: arkts.DiagnosticKind = arkts.DiagnosticKind.create( + message, + options.level === 'error' + ? arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_ERROR + : arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_WARNING + ); + if (options.fix) { + const diagnosticInfo: arkts.DiagnosticInfo = arkts.DiagnosticInfo.create(kind); + const fixSuggestion = options.fix(options.node); + const suggestionInfo: arkts.SuggestionInfo = arkts.SuggestionInfo.create(kind, fixSuggestion.code); + const [startPosition, endPosition] = fixSuggestion.range; + const sourceRange: arkts.SourceRange = arkts.SourceRange.create(startPosition, endPosition); + arkts.Diagnostic.logDiagnosticWithSuggestion(diagnosticInfo, suggestionInfo, sourceRange); + } else { + arkts.Diagnostic.logDiagnostic(kind, arkts.getStartPosition(options.node)); + } + + // todo + const position = arkts.getStartPosition(options.node); + if (options.fix) { + const suggestion = options.fix(options.node); + console.log(`syntax-${options.level ?? 'error'}: ${message}`); + console.log( + `range: (${suggestion.range[0].index()}, ${suggestion.range[0].line()}) - (${suggestion.range[1].index()}, ${suggestion.range[1].line()})`, + `code: ${suggestion.code}` + ); + } else { + console.log(`syntax-${options.level ?? 'error'}: ${message} (${position.index()},${position.line()})`); + } } - } - getMainPages(): string[] { - if (!this.projectConfig) { - return []; - } - const { moduleRootPath, aceModuleJsonPath } = this.projectConfig; - if (!aceModuleJsonPath) { - throw new Error('The aceModuleJsonPath config is empty.'); - } - const moduleConfig = readJSON(aceModuleJsonPath); - if (!moduleConfig.module || !moduleConfig.module.pages) { - throw new Error('Failed to read pages because the content of module.json5 is invalid.'); - } - const pagesPath = moduleConfig.module.pages; - const matcher = /\$(?[_A-Za-z]+):(?[_A-Za-z]+)/.exec(pagesPath); - if (matcher && matcher.groups) { - const { directory, filename } = matcher.groups; - const mainPagesPath = path.resolve(moduleRootPath, BASE_RESOURCE_PATH, directory, `${filename}.json`); - const mainPages = readJSON(mainPagesPath); - if (!mainPages.src || !Array.isArray(mainPages.src)) { - throw new Error(`Failed to read pages because the content of ${filename}.json is invalid.`); - } - return mainPages.src.map(page => path.resolve(moduleRootPath, ETS_PATH, `${page}.ets`)); - } else { - throw new Error('Failed to read pages from module.json5'); + getMainPages(): string[] { + if (!this.projectConfig) { + return []; + } + const { moduleRootPath, aceModuleJsonPath } = this.projectConfig; + if (!aceModuleJsonPath) { + throw new Error('The aceModuleJsonPath config is empty.'); + } + const moduleConfig = readJSON(aceModuleJsonPath); + if (!moduleConfig.module || !moduleConfig.module.pages) { + throw new Error('Failed to read pages because the content of module.json5 is invalid.'); + } + const pagesPath = moduleConfig.module.pages; + const matcher = /\$(?[_A-Za-z]+):(?[_A-Za-z]+)/.exec(pagesPath); + if (matcher && matcher.groups) { + const { directory, filename } = matcher.groups; + const mainPagesPath = path.resolve(moduleRootPath, BASE_RESOURCE_PATH, directory, `${filename}.json`); + const mainPages = readJSON(mainPagesPath); + if (!mainPages.src || !Array.isArray(mainPages.src)) { + throw new Error(`Failed to read pages because the content of ${filename}.json is invalid.`); + } + return mainPages.src.map((page) => path.resolve(moduleRootPath, ETS_PATH, `${page}.ets`)); + } else { + throw new Error('Failed to read pages from module.json5'); + } } - } - - private format(content: string, placeholders: object): string { - return Object.entries(placeholders).reduce( - (content, [placehoderName, placehoderValue]) => { - return content.replace(`{{${placehoderName}}}`, placehoderValue); - }, - content, - ); - } + private format(content: string, placeholders: object): string { + return Object.entries(placeholders).reduce((content, [placehoderName, placehoderValue]) => { + return content.replace(`{{${placehoderName}}}`, placehoderValue); + }, content); + } } class ConcreteUISyntaxRuleProcessor implements UISyntaxRuleProcessor { - protected context: UISyntaxRuleContext; - protected handlers: UISyntaxRuleHandler[]; - - constructor(rules: UISyntaxRule[]) { - this.context = new ConcreteUISyntaxRuleContext(); - this.handlers = rules.map(rule => { - return rule.setup(this.context); - }); - } - + protected context: UISyntaxRuleContext; + protected handlers: UISyntaxRuleHandler[]; + + constructor(rules: Array) { + this.context = new ConcreteUISyntaxRuleContext(); + this.handlers = rules.reduce((handlers, rule) => { + if (Array.isArray(rule)) { + const [RuleConstructor, level] = rule; + if (level !== 'none') { + handlers.push(new RuleConstructor(this.context, level)); + } + } else { + handlers.push(rule.setup(this.context)); + } + return handlers; + }, []); + } - parsed(node: arkts.AstNode): void { - for (const handlers of this.handlers) { - handlers.parsed?.(node); + parsed(node: arkts.AstNode): void { + for (const handlers of this.handlers) { + handlers.parsed?.(node); + } } - } - setProjectConfig(projectConfig: ProjectConfig): void { - this.context.projectConfig = projectConfig; - } + setProjectConfig(projectConfig: ProjectConfig): void { + this.context.projectConfig = projectConfig; + } } -export function createUISyntaxRuleProcessor( - rules: UISyntaxRule[], -): UISyntaxRuleProcessor { - return new ConcreteUISyntaxRuleProcessor(rules); +export function createUISyntaxRuleProcessor(rules: Array): UISyntaxRuleProcessor { + return new ConcreteUISyntaxRuleProcessor(rules); } 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 f8bbf624c..554230336 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/build-root-node.ts @@ -15,103 +15,87 @@ import * as arkts from '@koalaui/libarkts'; import { getIdentifierName, getAnnotationUsage, PresetDecorators } from '../utils'; -import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; +import { AbstractUISyntaxRule } from './ui-syntax-rule'; const BUILD_NAME: string = 'build'; -const BUILD_ROOT_NUM: number = 1; const STATEMENT_LENGTH: number = 1; -function isBuildOneRoot( - entryDecoratorUsage: arkts.AnnotationUsage | undefined, - statements: readonly arkts.Statement[], - buildNode: arkts.Identifier | undefined, - context: UISyntaxRuleContext): void { - if (statements.length > STATEMENT_LENGTH && buildNode) { - context.report({ - node: buildNode, - message: entryDecoratorUsage ? rule.messages.invalidBuildRoot : rule.messages.invalidBuildRootNode - }); - } -} - -function reportvalidBuildRoot( - entryDecoratorUsage: arkts.AnnotationUsage | undefined, - isContainer: boolean, - buildNode: arkts.Identifier | undefined, - context: UISyntaxRuleContext -): void { - if (entryDecoratorUsage && !isContainer && buildNode) { - context.report({ - node: buildNode, - message: rule.messages.invalidBuildRoot, - }); - } -} - -function checkBuildRootNode(node: arkts.AstNode, context: UISyntaxRuleContext): void { - const loadedContainerComponents = context.componentsInfo.containerComponents; - if (!arkts.isStructDeclaration(node)) { - return; - } - const entryDecoratorUsage = getAnnotationUsage(node, PresetDecorators.ENTRY); - node.definition.body.forEach(member => { - if (!arkts.isMethodDefinition(member) || getIdentifierName(member.name) !== BUILD_NAME) { - return; - } - const blockStatement = member.scriptFunction.body; - if (!blockStatement || !arkts.isBlockStatement(blockStatement)) { - return; - } - const buildNode = member.scriptFunction.id; - const statements = blockStatement.statements; - if (buildNode) { - // rule1: The 'build' method cannot have more than one root node. - isBuildOneRoot(entryDecoratorUsage, statements, buildNode, context); +class BuildRootNodeRule extends AbstractUISyntaxRule { + public setup(): Record { + return { + invalidEntryBuildRoot: `In an '@Entry' decorated component, the 'build' function can have only one root node, which must be a container component.`, + invalidBuildRoot: `The 'build' function can have only one root node.`, + }; } - if (statements.length !== BUILD_ROOT_NUM) { - return; - } - const expressionStatement = statements[0]; - if (!arkts.isExpressionStatement(expressionStatement)) { - return; - } - const callExpression = expressionStatement.expression; - if (!arkts.isCallExpression(callExpression)) { - return; + + public parsed(node: arkts.AstNode): void { + if (!arkts.isStructDeclaration(node)) { + return; + } + const entryDecoratorUsage = getAnnotationUsage(node, PresetDecorators.ENTRY); + node.definition.body.forEach((member) => { + if (!arkts.isMethodDefinition(member) || getIdentifierName(member.name) !== BUILD_NAME) { + return; + } + const blockStatement = member.scriptFunction.body; + if (!blockStatement || !arkts.isBlockStatement(blockStatement)) { + return; + } + const buildNode = member.scriptFunction.id; + if (!buildNode) { + return; + } + const statements = blockStatement.statements; + if (statements.length > STATEMENT_LENGTH) { + // rule1: The 'build' method cannot have more than one root node. + this.report({ + node: buildNode, + message: entryDecoratorUsage ? this.messages.invalidEntryBuildRoot : this.messages.invalidBuildRoot, + }); + } + if (!statements.length || !entryDecoratorUsage) { + return; + } + const expressionStatement = statements[0]; + if (!arkts.isExpressionStatement(expressionStatement)) { + return; + } + const callExpression = expressionStatement.expression; + if (!arkts.isCallExpression(callExpression)) { + return; + } + let componentName: string = this.getComponentName(callExpression); + let isContainer: boolean = this.isContainerComponent(componentName); + // rule2: its 'build' function can have only one root node, which must be a container component. + if (!isContainer) { + this.report({ + node: buildNode, + message: this.messages.invalidEntryBuildRoot, + }); + } + }); } - let componentName: string = ''; - if (arkts.isMemberExpression(callExpression.expression) && - arkts.isCallExpression(callExpression.expression.object) && - arkts.isIdentifier(callExpression.expression.object.expression)) { - componentName = getIdentifierName(callExpression.expression.object.expression); - } else if (arkts.isIdentifier(callExpression.expression)) { - componentName = getIdentifierName(callExpression.expression); + + private isContainerComponent(componentName: string): boolean { + const loadedContainerComponents = this.context.componentsInfo.containerComponents; + if (!componentName || !loadedContainerComponents) { + return false; + } + return loadedContainerComponents.includes(componentName); } - let isContainer: boolean = false; - if (!loadedContainerComponents) { - return; + + private getComponentName(callExpression: arkts.CallExpression): string { + if ( + arkts.isMemberExpression(callExpression.expression) && + arkts.isCallExpression(callExpression.expression.object) && + arkts.isIdentifier(callExpression.expression.object.expression) + ) { + return getIdentifierName(callExpression.expression.object.expression); + } else if (arkts.isIdentifier(callExpression.expression)) { + return getIdentifierName(callExpression.expression); + } + return ''; } - isContainer = componentName - ? loadedContainerComponents.includes(componentName) - : false; - // rule2: its 'build' function can have only one root node, which must be a container component. - reportvalidBuildRoot(entryDecoratorUsage, isContainer, buildNode, context); - }); } -const rule: UISyntaxRule = { - name: 'build-root-node', - messages: { - invalidBuildRoot: `In an '@Entry' decorated component, the 'build' function can have only one root node, which must be a container component.`, - invalidBuildRootNode: `The 'build' function can have only one root node.` - }, - setup(context) { - return { - parsed: (node): void => { - checkBuildRootNode(node, context); - }, - }; - }, -}; - -export default rule; +export default BuildRootNodeRule; diff --git a/arkui-plugins/ui-syntax-plugins/rules/index.ts b/arkui-plugins/ui-syntax-plugins/rules/index.ts index 4d1fa250a..e43742049 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/index.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/index.ts @@ -13,10 +13,10 @@ * limitations under the License. */ -import { UISyntaxRule } from './ui-syntax-rule'; +import { UISyntaxRule, UISyntaxRuleConfig } from './ui-syntax-rule'; import AttributeNoInvoke from './attribute-no-invoke'; import BuilderparamDecoratorCheck from './builderparam-decorator-check'; -import BuildRootNode from './build-root-node'; +import BuildRootNodeRule 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'; @@ -66,58 +66,58 @@ import ComponentComponentV2InitCheck from './component-componentV2-init-check'; import SpecificComponentChildren from './specific-component-children'; import StructNoExtends from './struct-no-extends'; -const rules: UISyntaxRule[] = [ - AttributeNoInvoke, - BuilderparamDecoratorCheck, - BuildRootNode, - CheckConstructPrivateParameter, - CheckDecoratedPropertyType, - ComponentComponentV2MixUseCheck, - ComponentV2MixCheck, - ConstructParameterLiteral, - ConstructParameter, - ConsumerProviderDecoratorCheck, - ComponentV2StateUsageValidation, - CustomDialogMissingController, - DecoratorsInUIComponentOnly, - EntryLoacalStorageCheck, - EntryStructNoExport, - LocalBuilderCheck, - MonitorDecoratorCheck, - NestedRelationship, - NoChildInButton, - NoDuplicateDecorators, - NoDuplicateEntry, - NoDuplicateId, - NoDuplicatePreview, - NoDuplicateStateManager, - NoPropLinkObjectlinkInEntry, - NoSameAsBuiltInAttribute, - ReuseAttributeCheck, - StructMissingDecorator, - StructPropertyDecorator, - StructVariableInitialization, - TrackDecoratorCheck, - TypeDecoratorCheck, - ValidateBuildInStruct, - VariableInitializationViaComponentCons, - WatchDecoratorFunction, - WatchDecoratorRegular, - WrapBuilderCheck, - ObservedHeritageCompatibleCheck, - ObservedObservedV2, - ObservedV2TraceUsageValidation, - OnceDecoratorCheck, - OneDecoratorOnFunctionMethod, - OldNewDecoratorMixUseCheck, - ComputedDecoratorCheck, - ReusableV2DecoratorCheck, - RequireDecoratorRegular, - ReusableComponentInV2Check, - VariableInitializationViaComponentConstructor, - ComponentComponentV2InitCheck, - SpecificComponentChildren, - StructNoExtends, +const rules: Array = [ + AttributeNoInvoke, + BuilderparamDecoratorCheck, + [BuildRootNodeRule, 'error'], + CheckConstructPrivateParameter, + CheckDecoratedPropertyType, + ComponentComponentV2MixUseCheck, + ComponentV2MixCheck, + ConstructParameterLiteral, + ConstructParameter, + ConsumerProviderDecoratorCheck, + ComponentV2StateUsageValidation, + CustomDialogMissingController, + DecoratorsInUIComponentOnly, + EntryLoacalStorageCheck, + EntryStructNoExport, + LocalBuilderCheck, + MonitorDecoratorCheck, + NestedRelationship, + NoChildInButton, + NoDuplicateDecorators, + NoDuplicateEntry, + NoDuplicateId, + NoDuplicatePreview, + NoDuplicateStateManager, + NoPropLinkObjectlinkInEntry, + NoSameAsBuiltInAttribute, + ReuseAttributeCheck, + StructMissingDecorator, + StructPropertyDecorator, + StructVariableInitialization, + TrackDecoratorCheck, + TypeDecoratorCheck, + ValidateBuildInStruct, + VariableInitializationViaComponentCons, + WatchDecoratorFunction, + WatchDecoratorRegular, + WrapBuilderCheck, + ObservedHeritageCompatibleCheck, + ObservedObservedV2, + ObservedV2TraceUsageValidation, + OnceDecoratorCheck, + OneDecoratorOnFunctionMethod, + OldNewDecoratorMixUseCheck, + ComputedDecoratorCheck, + ReusableV2DecoratorCheck, + RequireDecoratorRegular, + ReusableComponentInV2Check, + VariableInitializationViaComponentConstructor, + ComponentComponentV2InitCheck, + SpecificComponentChildren, + StructNoExtends, ]; export default rules; diff --git a/arkui-plugins/ui-syntax-plugins/rules/ui-syntax-rule.ts b/arkui-plugins/ui-syntax-plugins/rules/ui-syntax-rule.ts index e62fa514b..47774e2db 100644 --- a/arkui-plugins/ui-syntax-plugins/rules/ui-syntax-rule.ts +++ b/arkui-plugins/ui-syntax-plugins/rules/ui-syntax-rule.ts @@ -18,32 +18,67 @@ import { ProjectConfig } from 'common/plugin-context'; import { UISyntaxRuleComponents } from 'ui-syntax-plugins/utils'; export type FixSuggestion = { - range: [start: arkts.SourcePosition, end: arkts.SourcePosition]; - code: string; + range: [start: arkts.SourcePosition, end: arkts.SourcePosition]; + code: string; }; export type ReportOptions = { - node: arkts.AstNode; - message: string; - data?: Record; - fix?: (node: arkts.AstNode) => FixSuggestion; + node: arkts.AstNode; + message: string; + data?: Record; + fix?: (node: arkts.AstNode) => FixSuggestion; + level?: UISyntaxRuleLevel; }; export type UISyntaxRuleContext = { - projectConfig?: ProjectConfig; - componentsInfo: UISyntaxRuleComponents; - report(options: ReportOptions): void; - getMainPages(): string[]; + projectConfig?: ProjectConfig; + componentsInfo: UISyntaxRuleComponents; + report(options: ReportOptions): void; + getMainPages(): string[]; }; export type UISyntaxRulePhaseHandler = (node: arkts.AstNode) => void; export type UISyntaxRuleHandler = { - parsed?: UISyntaxRulePhaseHandler; + parsed?: UISyntaxRulePhaseHandler; }; export type UISyntaxRule = { - name: string; - messages: Record; - setup(context: UISyntaxRuleContext): UISyntaxRuleHandler; + name: string; + messages: Record; + setup(context: UISyntaxRuleContext): UISyntaxRuleHandler; }; + +export type UISyntaxRuleReportOptions = { + node: arkts.AstNode; + message: string; + data?: Record; + fix?: (node: arkts.AstNode) => FixSuggestion; +}; + +export type UISyntaxRuleLevel = 'error' | 'warn' | 'none'; + +export interface UISyntaxRuleConstructor { + new (context: UISyntaxRuleContext, level: UISyntaxRuleLevel): AbstractUISyntaxRule; +} + +export abstract class AbstractUISyntaxRule { + protected messages: Record; + + constructor(protected context: UISyntaxRuleContext, protected level: UISyntaxRuleLevel) { + this.messages = this.setup(); + } + + public parsed(node: arkts.AstNode): void {} + public binded(node: arkts.AstNode): void {} + public abstract setup(): Record; + + protected report(options: UISyntaxRuleReportOptions): void { + this.context.report({ + ...options, + level: this.level, + }); + } +} + +export type UISyntaxRuleConfig = [UISyntaxRuleConstructor, UISyntaxRuleLevel]; -- Gitee