From a95deff95a2f517031586c78a7b67fcc8929f57a Mon Sep 17 00:00:00 2001 From: zhangzezhong Date: Fri, 6 Jun 2025 19:24:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=84=8F=E5=9B=BE=E6=A1=86=E6=9E=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9EForm=E8=A3=85=E9=A5=B0=E5=99=A8=E4=B8=8EEntity?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: zhangzezhong --- .../ets_ui/rollup-plugin-ets-typescript.ts | 1 - compiler/src/pre_define.ts | 2 + compiler/src/process_ui_syntax.ts | 4 +- .../src/userIntents_parser/intentLogger.ts | 27 +- compiler/src/userIntents_parser/intentType.ts | 47 +- .../userIntents_parser/parseUserIntents.ts | 1153 +++++++++++++---- .../@InsightIntentEntity.js.sample | 26 + .../@InsightIntentForm.js.sample | 49 + .../@InsightIntentEntity.ets | 44 + .../@InsightIntentForm/@InsightIntentForm.ets | 92 ++ .../transform_ut/helpers/insightUTPath.ts | 4 +- 11 files changed, 1144 insertions(+), 305 deletions(-) create mode 100644 compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.js.sample create mode 100644 compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.js.sample create mode 100644 compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.ets create mode 100644 compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.ets diff --git a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts index f1e30ae7b..3c11e1d89 100644 --- a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts +++ b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts @@ -231,7 +231,6 @@ export function etsTransform() { const hookEventFactory: CompileEvent = getHookEventFactory(this.share, 'etsTransform', 'afterBuildEnd'); const eventEtsTransformAfterBuildEnd = createAndStartEvent(hookEventFactory, 'etsTransformafterBuildEnd'); if (parseIntent.intentData.length > 0 || parseIntent.isUpdateCompile) { - parseIntent.verifyInheritanceChain(); parseIntent.writeUserIntentJsonFile(); } // Copy the cache files in the compileArkTS directory to the loader_out directory diff --git a/compiler/src/pre_define.ts b/compiler/src/pre_define.ts index 7b041ad59..82109acda 100644 --- a/compiler/src/pre_define.ts +++ b/compiler/src/pre_define.ts @@ -98,6 +98,8 @@ export const COMPONENT_USER_INTENTS_DECORATOR_ENTRY: string = '@InsightIntentEnt export const COMPONENT_USER_INTENTS_DECORATOR_FUNCTION: string = '@InsightIntentFunction'; export const COMPONENT_USER_INTENTS_DECORATOR_METHOD: string = '@InsightIntentFunctionMethod'; export const COMPONENT_USER_INTENTS_DECORATOR_PAGE: string = '@InsightIntentPage'; +export const COMPONENT_USER_INTENTS_DECORATOR_ENTITY: string = '@InsightIntentEntity'; +export const COMPONENT_USER_INTENTS_DECORATOR_FORM: string = '@InsightIntentForm'; export const CHECK_COMPONENT_EXTEND_DECORATOR: string = 'Extend'; export const STRUCT_CONTEXT_METHOD_DECORATORS: Set = new Set([COMPONENT_BUILDER_DECORATOR, COMPONENT_STYLES_DECORATOR, COMPONENT_LOCAL_BUILDER_DECORATOR]); diff --git a/compiler/src/process_ui_syntax.ts b/compiler/src/process_ui_syntax.ts index 745b53a78..72ec22d69 100644 --- a/compiler/src/process_ui_syntax.ts +++ b/compiler/src/process_ui_syntax.ts @@ -301,7 +301,7 @@ export function processUISyntax(program: ts.Program, ut = false, } if (ts.isStructDeclaration(node)) { // This processing aims to parse InsightIntentDecorator. - node = parseIntent.detectInsightIntent(node, metaInfo, filePath, eventProcessUISyntax); + node = parseIntent.detectInsightIntent(node, metaInfo, filePath, eventProcessUISyntax, transformLog.errors); hasStruct = true; componentCollection.currentClassName = node.name.getText(); componentCollection.entryComponent === componentCollection.currentClassName && entryKeyNode(node); @@ -412,7 +412,7 @@ export function processUISyntax(program: ts.Program, ut = false, } } else if (ts.isClassDeclaration(node)) { // This processing aims to parse InsightIntentDecorator. - node = parseIntent.detectInsightIntent(node, metaInfo, filePath, eventProcessUISyntax); + node = parseIntent.detectInsightIntent(node, metaInfo, filePath, eventProcessUISyntax, transformLog.errors); if (hasDecorator(node, COMPONENT_SENDABLE_DECORATOR)) { if (projectConfig.compileHar && !projectConfig.useTsHar) { let warnMessage: string = 'If you use @Sendable in js har, an exception will occur during runtime.\n' + diff --git a/compiler/src/userIntents_parser/intentLogger.ts b/compiler/src/userIntents_parser/intentLogger.ts index 40848c1c8..23545f70c 100644 --- a/compiler/src/userIntents_parser/intentLogger.ts +++ b/compiler/src/userIntents_parser/intentLogger.ts @@ -83,29 +83,4 @@ export class IntentLogger { const message: string = typeof error === 'string' ? error : error.toString(); throw new Error(message); } -} - -export const ENTRYPATH_ERROR: LogData = LogDataFactory.newInstance('1001', '[InsightIntent] IntentDecorator needs to be in the .ets file'); -export const DECORATOR_STATEMENT_ERROR: LogData = LogDataFactory.newInstance('1002', '[InsightIntent] decorator is not CallExpression'); -export const DYNAMIC_PARAM_ERROR: LogData = LogDataFactory.newInstance('1003', '[InsightIntent] Dynamic variable cannot be resolved'); -export const DISALLOWED_PARAM_ERROR: LogData = LogDataFactory.newInstance('1004', '[InsightIntent] param is disallowed'); -export const UNSUPPORTED_PARSE_ERROR: LogData = LogDataFactory.newInstance('1005', '[InsightIntent] unsupported parameter type cannot be parsed'); -export const INCORRECT_PARAM_TYPE_ERROR: LogData = LogDataFactory.newInstance('1006', '[InsightIntent] param parsing occurs error param type'); -export const REQUIRED_PARAM_DISMISS_ERROR: LogData = LogDataFactory.newInstance('1007', '[InsightIntent] decorator args missing required param'); -export const INTERNAL_ERROR: LogData = LogDataFactory.newInstance('1008', '[InsightIntent] internal error'); -export const SCHEMA_VALIDATE_ONE_OF_ERROR: LogData = LogDataFactory.newInstance('1009', '[InsightIntent] Not meeting the one of schema verification rules'); -export const SCHEMA_VALIDATE_ANY_OF_ERROR: LogData = LogDataFactory.newInstance('1010', '[InsightIntent] Not meeting the any of schema verification rules'); -export const SCHEMA_VALIDATE_TYPE_ERROR: LogData = LogDataFactory.newInstance('1011', '[InsightIntent] schema verification parameter type error'); -export const SCHEMA_VALIDATE_REQUIRED_ERROR: LogData = LogDataFactory.newInstance('1012', - '[InsightIntent] schema verification required parameter does not exist'); -export const SCHEMA_ROOT_TYPE_MISMATCH_ERROR: LogData = LogDataFactory.newInstance('1013', '[InsightIntent] Schema root type must be \'object\''); -export const INVALID_BASE_CLASS_ERROR: LogData = LogDataFactory.newInstance('1014', - '[InsightIntent] decorated with @InsightIntentEntry has an invalid inheritance hierarchy.'); -export const PARAM_CIRCULAR_REFERENCE_ERROR: LogData = LogDataFactory.newInstance('1015', - '[InsightIntent] Circular reference detected in param'); -export const INVALID_PAGEPATH_ERROR: LogData = LogDataFactory.newInstance('1016', - '[InsightIntent] @InsightIntentPage Resolved \'pagePath\' path not found in project directory'); -export const DECORATOR_ILLEGAL_USE: LogData = LogDataFactory.newInstance('1017', - '[InsightIntent] @InsightIntentFunctionMethod must be declared under the @InsightIntentFunction decorator'); -export const DECORATOR_DUPLICATE_INTENTNAME: LogData = LogDataFactory.newInstance('1018', - '[InsightIntent] user intents has duplicate intentName param'); +} \ No newline at end of file diff --git a/compiler/src/userIntents_parser/intentType.ts b/compiler/src/userIntents_parser/intentType.ts index bfe00c16e..a29d16a24 100644 --- a/compiler/src/userIntents_parser/intentType.ts +++ b/compiler/src/userIntents_parser/intentType.ts @@ -60,6 +60,15 @@ export interface IntentPageInfo extends IntentInfo { navDestinationName: string; } +export interface IntentEntityDecoratorInfo { + entityCategory: string; + parameters?: object; +} + +export interface FormIntentDecoratorInfo extends IntentInfo { + formName: string; +} + export class ParamChecker { private _requiredFields: (keyof T)[]; private _allowFields: Set; @@ -193,7 +202,7 @@ IntentLinkInfoChecker.paramValidators = { intentVersion: validateRequiredString, displayName: validateRequiredString, displayDescription: validateOptionalString, - schema: validateRequiredString, + schema: validateOptionalString, icon: validateIcon, llmDescription: validateOptionalString, uri: validateRequiredString @@ -209,7 +218,7 @@ intentEntryInfoChecker.paramValidators = { parameters: validateParameters, icon: validateIcon, keywords: validateKeywords, - schema: validateRequiredString, + schema: validateOptionalString, abilityName: validateRequiredString, displayDescription: validateRequiredString, displayName: validateRequiredString, @@ -243,7 +252,7 @@ intentMethodInfoChecker.paramValidators = { parameters: validateParameters, icon: validateIcon, keywords: validateKeywords, - schema: validateRequiredString, + schema: validateOptionalString, intentName: validateRequiredString, domain: validateRequiredString, intentVersion: validateRequiredString, @@ -268,7 +277,7 @@ IntentPageInfoChecker.paramValidators = { parameters: validateParameters, icon: validateIcon, keywords: validateKeywords, - schema: validateRequiredString, + schema: validateOptionalString, intentName: validateRequiredString, domain: validateRequiredString, intentVersion: validateRequiredString, @@ -280,3 +289,33 @@ IntentPageInfoChecker.paramValidators = { navigationId: validateOptionalString, navDestinationName: validateOptionalString }; + +export const IntentEntityInfoChecker: ParamChecker = new ParamChecker(); +IntentEntityInfoChecker.requiredFields = ['entityCategory']; +IntentEntityInfoChecker.allowFields = new Set(['entityCategory', 'parameters']); + +IntentEntityInfoChecker.paramValidators = { + entityCategory: validateOptionalString, + parameters: validateParameters +}; + +export const intentFormInfoChecker = new ParamChecker(); +intentFormInfoChecker.requiredFields = [...BASE_REQUIRED as Array, 'formName']; +intentFormInfoChecker.allowFields = new Set([ + ...BASE_ALLOW as Array, 'formName' +]); +intentFormInfoChecker.paramValidators = { + formName: validateOptionalString, + example: validateOptionalString, + result: validateParameters, + parameters: validateParameters, + icon: validateIcon, + keywords: validateKeywords, + schema: validateRequiredString, + intentName: validateRequiredString, + domain: validateRequiredString, + intentVersion: validateRequiredString, + displayName: validateRequiredString, + displayDescription: validateOptionalString, + llmDescription: validateOptionalString +}; diff --git a/compiler/src/userIntents_parser/parseUserIntents.ts b/compiler/src/userIntents_parser/parseUserIntents.ts index 081f3234c..b98525db1 100644 --- a/compiler/src/userIntents_parser/parseUserIntents.ts +++ b/compiler/src/userIntents_parser/parseUserIntents.ts @@ -18,48 +18,32 @@ import { IntentEntryInfo, intentEntryInfoChecker, IntentLinkInfo, - IntentLinkInfoChecker, intentMethodInfoChecker, + IntentLinkInfoChecker, + intentMethodInfoChecker, LinkIntentParamMapping, IntentPageInfoChecker, - ParamChecker + ParamChecker, + IntentEntityInfoChecker, + intentFormInfoChecker } from './intentType'; -import { - DECORATOR_DUPLICATE_INTENTNAME, - DECORATOR_ILLEGAL_USE, - DECORATOR_STATEMENT_ERROR, - DISALLOWED_PARAM_ERROR, - DYNAMIC_PARAM_ERROR, - ENTRYPATH_ERROR, - INCORRECT_PARAM_TYPE_ERROR, - IntentLogger, - INTERNAL_ERROR, - INVALID_BASE_CLASS_ERROR, INVALID_PAGEPATH_ERROR, - PARAM_CIRCULAR_REFERENCE_ERROR, - REQUIRED_PARAM_DISMISS_ERROR, - SCHEMA_ROOT_TYPE_MISMATCH_ERROR, - SCHEMA_VALIDATE_ANY_OF_ERROR, - SCHEMA_VALIDATE_ONE_OF_ERROR, - SCHEMA_VALIDATE_REQUIRED_ERROR, - SCHEMA_VALIDATE_TYPE_ERROR, - UNSUPPORTED_PARSE_ERROR -} from './intentLogger'; +import { IntentLogger } from './intentLogger'; import path from 'path'; import { getNormalizedOhmUrlByFilepath } from '../ark_utils'; import { globalModulePaths, projectConfig } from '../../main'; import fs from 'fs'; +import json5 from 'json5'; import { ProjectCollections } from 'arkguard'; import { COMPONENT_USER_INTENTS_DECORATOR, + COMPONENT_USER_INTENTS_DECORATOR_ENTITY, COMPONENT_USER_INTENTS_DECORATOR_ENTRY, COMPONENT_USER_INTENTS_DECORATOR_FUNCTION, COMPONENT_USER_INTENTS_DECORATOR_METHOD, - COMPONENT_USER_INTENTS_DECORATOR_PAGE + COMPONENT_USER_INTENTS_DECORATOR_PAGE, + COMPONENT_USER_INTENTS_DECORATOR_FORM } from '../pre_define'; -import { - CompileEvent, - createAndStartEvent, - stopEvent -} from '../performance'; +import { CompileEvent, createAndStartEvent, stopEvent } from '../performance'; +import { LogInfo, LogType } from '../utils'; type StaticValue = string | number | boolean | null | undefined | StaticValue[] | { [key: string]: StaticValue }; @@ -70,23 +54,36 @@ interface methodParametersInfo { } class ParseIntent { + private checker: ts.TypeChecker; + public intentData: object[]; + private currentFilePath: string; + private heritageClassSet: Set; + private updatePageIntentObj: Map; + public isUpdateCompile: boolean = true; + private isInitCache: boolean = false; + private entityMap: Map; + private entityOwnerMap: Map; + private moduleJsonInfo: Map; + private EntityHeritageClassSet: Set; + private EntityExtendsMap: Map; + private transformLog: LogInfo[]; + private currentNode: ts.Node; + constructor() { this.intentData = []; this.currentFilePath = ''; this.heritageClassSet = new Set(); + this.heritageClassSet.add('IntentEntity_sdk'); this.heritageClassSet.add('InsightIntentEntryExecutor_sdk'); this.updatePageIntentObj = new Map(); + this.entityMap = new Map(); + this.entityOwnerMap = new Map(); + this.moduleJsonInfo = new Map(); + this.EntityHeritageClassSet = new Set(); + this.EntityExtendsMap = new Map(); } - checker: ts.TypeChecker; - intentData: object[]; - currentFilePath: string; - heritageClassSet: Set; - updatePageIntentObj: Map; - isUpdateCompile: boolean = false; - isInitCache : boolean = false; - - hasDecorator(node: ts.Node, decorators: string[]): boolean { + private hasDecorator(node: ts.Node, decorators: string[]): boolean { if (!node.modifiers) { return false; } @@ -96,37 +93,32 @@ class ParseIntent { } let decoratorName: string | undefined; if (ts.isCallExpression(decorator.expression)) { - decoratorName = '@' + decorator.expression.expression.getText(); + decoratorName = `@${decorator.expression.expression.getText()}`; } return decoratorName !== undefined && decorators.includes(decoratorName); }); } - detectInsightIntent(node: ts.ClassDeclaration, metaInfo: object, filePath: string, eventOrEventFactory: CompileEvent | undefined): ts.Node { + public detectInsightIntent( + node: ts.ClassDeclaration, metaInfo: object, filePath: string, eventOrEventFactory: CompileEvent | undefined, transformLog: LogInfo[]): ts.Node { + this.initInsightIntent(node, metaInfo, transformLog, filePath); const eventParseIntentTime: CompileEvent | undefined = createAndStartEvent(eventOrEventFactory, 'parseIntentTime'); - if (!this.isInitCache && projectConfig.cachePath) { - const cacheSourceMapPath: string = path.join(projectConfig.cachePath, 'insight_compile_cache.json'); - this.isUpdateCompile = fs.existsSync(cacheSourceMapPath); - this.isInitCache = true; - } - if (this.isUpdateCompile) { - const pkgParams: object = { - pkgName: metaInfo.pkgName, - pkgPath: metaInfo.pkgPath - }; - const Logger: IntentLogger = IntentLogger.getInstance(); - const recordName: string = getNormalizedOhmUrlByFilepath(filePath, projectConfig, Logger, pkgParams, null); - if (!this.updatePageIntentObj.has(`@normalized:${recordName}`)) { - this.updatePageIntentObj.set(`@normalized:${recordName}`, []); - } - } const definedDecorators: string[] = [COMPONENT_USER_INTENTS_DECORATOR, COMPONENT_USER_INTENTS_DECORATOR_ENTRY, - COMPONENT_USER_INTENTS_DECORATOR_FUNCTION, COMPONENT_USER_INTENTS_DECORATOR_PAGE]; + COMPONENT_USER_INTENTS_DECORATOR_FUNCTION, COMPONENT_USER_INTENTS_DECORATOR_PAGE, COMPONENT_USER_INTENTS_DECORATOR_ENTITY, + COMPONENT_USER_INTENTS_DECORATOR_FORM]; if (ts.isClassDeclaration(node) && !this.hasDecorator(node, [COMPONENT_USER_INTENTS_DECORATOR_FUNCTION])) { node.members.forEach((member) => { if (ts.isMethodDeclaration(member) && this.hasModifier(member, ts.SyntaxKind.StaticKeyword) && this.hasDecorator(member, [COMPONENT_USER_INTENTS_DECORATOR_METHOD])) { - throw Error(`${DECORATOR_ILLEGAL_USE.toString()}, className: ${node.name.getText()}`); + const errorMessage: string = '@InsightIntentFunctionMethod must be declared under the @InsightIntentFunction decorator'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: node.getStart(), + code: '10105110', + description: 'ArkTs InsightIntent Error' + }); + return; } }); } @@ -139,60 +131,195 @@ class ParseIntent { return node; } - handleIntent(node: ts.ClassDeclaration, checker: ts.TypeChecker, filepath: string, metaInfo: Object = {}): void { + private initInsightIntent(node: ts.ClassDeclaration, metaInfo: object, transformLog: LogInfo[], filePath: string): void { + this.transformLog = transformLog; + this.currentNode = node; + if (!this.isInitCache) { + if (projectConfig.cachePath) { + const cacheSourceMapPath: string = + path.join(projectConfig.cachePath, 'insight_compile_cache.json'); // The user's intents configuration file + this.isUpdateCompile = fs.existsSync(cacheSourceMapPath); + this.isInitCache = true; + } else { + this.isUpdateCompile = false; + } + } + if (this.isUpdateCompile) { + const pkgParams: object = { + pkgName: metaInfo.pkgName, + pkgPath: metaInfo.pkgPath + }; + const Logger: IntentLogger = IntentLogger.getInstance(); + const recordName: string = getNormalizedOhmUrlByFilepath(filePath, projectConfig, Logger, pkgParams, null); + if (!this.updatePageIntentObj.has(`@normalized:${recordName}`)) { + this.updatePageIntentObj.set(`@normalized:${recordName}`, []); + } + } + } + + private handleIntent(node: ts.ClassDeclaration, checker: ts.TypeChecker, filepath: string, metaInfo: Object = {}): void { this.checker = checker; this.currentFilePath = filepath; if (!filepath.endsWith('.ets')) { - throw Error(`${ENTRYPATH_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = 'IntentDecorator needs to be in the .ets file'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101002', + description: 'ArkTs InsightIntent Error' + }); + return; } const pkgParams: object = { pkgName: metaInfo.pkgName, pkgPath: metaInfo.pkgPath }; node.modifiers.forEach(decorator => { - const expr: ts.Expression = decorator.expression; - if (!expr || !ts.isCallExpression(expr)) { - return; + this.handleDecorator(node, decorator, filepath, pkgParams); + }); + } + + private handleDecorator(node: ts.ClassDeclaration, decorator: ts.Decorator, filepath: string, pkgParams: object): void { + const expr: ts.Expression = decorator.expression; + if (!expr || !ts.isCallExpression(expr)) { + return; + } + const argumentKind: ts.SyntaxKind | undefined = expr.arguments[0]?.kind; + if (argumentKind && argumentKind === ts.SyntaxKind.NullKeyword || argumentKind && argumentKind === ts.SyntaxKind.UndefinedKeyword) { + return; + } + const symbol: ts.Symbol = this.checker.getTypeAtLocation(decorator.expression.expression)?.getSymbol(); + const declarations: ts.Declaration[] | undefined = symbol?.getDeclarations(); + if (!declarations || declarations.length === 0) { + return; + } + const decoratorSourceFile: string = declarations[0].getSourceFile().fileName; + const isGlobalPathFlag: boolean = this.isGlobalPath(decoratorSourceFile); + if (!isGlobalPathFlag) { + return; + } + const Logger: IntentLogger = IntentLogger.getInstance(); + const recordName: string = getNormalizedOhmUrlByFilepath(filepath, projectConfig, Logger, pkgParams, null); + const intentObj: object = { + 'decoratorFile': `@normalized:${recordName}`, + 'decoratorClass': node.name.text + }; + const originalDecorator: string = '@' + decorator.expression.expression.getText(); + if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR) { + this.handleLinkDecorator(intentObj, node, decorator); + } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_ENTRY) { + this.handleEntryDecorator(intentObj, node, decorator, pkgParams); + } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_FUNCTION) { + this.handleMethodDecorator(intentObj, node, decorator); + } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_PAGE) { + this.handlePageDecorator(intentObj, node, decorator, pkgParams); + } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_ENTITY) { + this.handleEntityDecorator(intentObj, node, decorator, pkgParams); + } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_FORM) { + this.handleFormDecorator(intentObj, node, decorator, pkgParams); + } + } + + private handleFormDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator, + pkgParams: object): void { + const expr: ts.Expression = decorator.expression; + if (ts.isCallExpression(expr)) { + const args: ts.NodeArray = expr.arguments; + Object.assign(intentObj, { + 'bundleName': projectConfig.bundleName, + 'moduleName': projectConfig.moduleName, + 'decoratorType': COMPONENT_USER_INTENTS_DECORATOR_FORM + }); + this.analyzeDecoratorArgs(args, intentObj, intentFormInfoChecker); + const properties: Record = this.parseClassNode(node, intentObj.intentName); + this.processFormInfo(node, this.currentFilePath, pkgParams, intentObj); + this.schemaValidateSync(properties, intentObj.parameters); + this.createObfuscation(node); + if (this.isUpdateCompile) { + this.updatePageIntentObj.get(intentObj.decoratorFile).push(intentObj); } - const argumentKind: ts.SyntaxKin | undefined = expr.arguments[0]?.kind; - if (argumentKind && argumentKind === ts.SyntaxKind.NullKeyword || argumentKind && argumentKind === ts.SyntaxKind.UndefinedKeyword) { - return; + this.intentData.push(intentObj); + } else { + const errorMessage: string = 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + + private handleEntityDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator, + pkgParams: object): void { + const entityClassName: string = this.checker.getTypeAtLocation(node).getSymbol().getName(); + const expr: ts.Expression = decorator.expression; + if (ts.isCallExpression(expr)) { + const args: ts.NodeArray = expr.arguments; + Object.assign(intentObj, { + 'decoratorType': COMPONENT_USER_INTENTS_DECORATOR_ENTITY, + 'className': intentObj.decoratorClass + }); + delete intentObj.decoratorClass; + this.analyzeDecoratorArgs(args, intentObj, IntentEntityInfoChecker); + const properties: Record = this.parseClassNode(node, undefined); + const entityId: string = this.getEntityId(node); + Object.assign(properties, { + 'entityId': entityId + }); + Object.assign(intentObj, { + 'entityId': entityId + }); + this.schemaValidateSync(properties, intentObj.parameters); + this.analyzeBaseClass(node, pkgParams, intentObj, COMPONENT_USER_INTENTS_DECORATOR_ENTITY); + this.createObfuscation(node); + if (this.isUpdateCompile) { + this.updatePageIntentObj.get(intentObj.decoratorFile).push(intentObj); } - const symbol: ts.Symbol = checker.getTypeAtLocation(decorator.expression.expression)?.getSymbol(); - const declarations: ts.Declaration[] | undefined = symbol?.getDeclarations(); - if (!declarations || declarations.length === 0) { + if (this.entityMap.has(entityClassName)) { + const errorMessage: string = 'class can be decorated with at most one @InsightIntentEntity'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101004', + description: 'ArkTs InsightIntent Error' + }); return; + } else { + this.entityMap.set(entityClassName, intentObj); } - const decoratorSourceFile: string = declarations[0].getSourceFile().fileName; - const isGlobalPath: boolean = globalModulePaths?.some(globalPath => { - const normalizedParent: string = path.normalize(decoratorSourceFile).replace(/\\/g, '/'); - const normalizedGlobalPath: string = path.normalize(globalPath).replace(/\\/g, '/'); - return normalizedParent.startsWith(normalizedGlobalPath); + } else { + const errorMessage: string = '10101003 ArkTs InsightIntent Error\n' + + 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003' }); - if (!isGlobalPath) { - return; - } - const Logger: IntentLogger = IntentLogger.getInstance(); - const recordName: string = getNormalizedOhmUrlByFilepath(filepath, projectConfig, Logger, pkgParams, null); - const intentObj: object = { - 'decoratorFile': `@normalized:${recordName}`, - 'decoratorClass': node.name.text - }; - const originalDecorator: string = '@' + decorator.expression.expression.getText(); - if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR) { - this.handleLinkDecorator(intentObj, node, decorator); - } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_ENTRY) { - this.handleEntryDecorator(intentObj, node, decorator, pkgParams); - } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_FUNCTION) { - this.handleMethodDecorator(intentObj, node, decorator); - } else if (originalDecorator === COMPONENT_USER_INTENTS_DECORATOR_PAGE) { - this.handlePageDecorator(intentObj, node, decorator, pkgParams); - } - }); + return; + } } - handlePageDecorator(intentObj: object, node: ts.Node, decorator: ts.Decorator, pkgParams: object): void { + private handlePageDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator, + pkgParams: object): void { const expr: ts.Expression = decorator.expression; + if (ts.isClassDeclaration(node)) { + const errorMessage: string = `@InsightIntentPage must be decorated on struct` + + `, className: ${node.name.getText()}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; + } if (ts.isCallExpression(expr)) { const args: ts.NodeArray = expr.arguments; Object.assign(intentObj, { @@ -201,18 +328,26 @@ class ParseIntent { 'decoratorType': COMPONENT_USER_INTENTS_DECORATOR_PAGE }); this.analyzeDecoratorArgs(args, intentObj, IntentPageInfoChecker); + this.validatePagePath(intentObj, pkgParams); this.createObfuscation(node); - this.transformPagePath(intentObj, pkgParams); if (this.isUpdateCompile) { this.updatePageIntentObj.get(intentObj.decoratorFile).push(intentObj); } this.intentData.push(intentObj); } else { - throw Error(`${DECORATOR_STATEMENT_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; } } - handleMethodDecorator(intentObj: object, node: ts.Node, decorator: ts.Decorator): void { + private handleMethodDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator): void { const expr: ts.Expression = decorator.expression; if (ts.isCallExpression(expr)) { Object.assign(intentObj, { @@ -227,13 +362,15 @@ class ParseIntent { args: ts.NodeArray, } - const methodParameters: methodParametersInfo[] = this.parseClassMethods(node, COMPONENT_USER_INTENTS_DECORATOR_METHOD); + const methodParameters: methodParametersInfo[] = + this.parseClassMethods(node, COMPONENT_USER_INTENTS_DECORATOR_METHOD); methodParameters.forEach(methodDecorator => { const functionName: string = methodDecorator.functionName; const methodArgs: ts.NodeArray = methodDecorator.args; const properties: Record = methodDecorator.parameters; const functionParamList: Array = Object.keys(properties); - const methodObj: object = Object.assign({}, intentObj, {functionName, 'functionParamList': functionParamList}); + const methodObj: object = + Object.assign({}, intentObj, { functionName, 'functionParamList': functionParamList }); this.analyzeDecoratorArgs(methodArgs, methodObj, intentMethodInfoChecker); if (this.isUpdateCompile) { this.updatePageIntentObj.get(methodObj.decoratorFile).push(methodObj); @@ -242,11 +379,19 @@ class ParseIntent { }); this.createObfuscation(node); } else { - throw Error(`${DECORATOR_STATEMENT_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; } } - handleLinkDecorator(intentObj: object, node: ts.Node, decorator: ts.Decorator): void { + private handleLinkDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator): void { const expr: ts.Expression = decorator.expression; if (ts.isCallExpression(expr)) { const args: ts.NodeArray = expr.arguments; @@ -262,11 +407,20 @@ class ParseIntent { } this.intentData.push(intentObj); } else { - throw Error(`${DECORATOR_STATEMENT_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; } } - handleEntryDecorator(intentObj: object, node: ts.Node, decorator: ts.Decorator, pkgParams: object): void { + private handleEntryDecorator(intentObj: object, node: ts.ClassDeclaration, decorator: ts.Decorator, + pkgParams: object): void { const expr: ts.Expression = decorator.expression; if (ts.isCallExpression(expr)) { const args: ts.NodeArray = expr.arguments; @@ -276,9 +430,9 @@ class ParseIntent { 'decoratorType': COMPONENT_USER_INTENTS_DECORATOR_ENTRY }); this.analyzeDecoratorArgs(args, intentObj, intentEntryInfoChecker); - const properties: Record = this.parseClassNode(node); + const properties: Record = this.parseClassNode(node, intentObj.intentName); this.schemaValidateSync(properties, intentObj.parameters); - this.analyzeBaseClass(node, pkgParams, intentObj); + this.analyzeBaseClass(node, pkgParams, intentObj, COMPONENT_USER_INTENTS_DECORATOR_ENTRY); this.createObfuscation(node); this.processExecuteModeParam(intentObj); if (this.isUpdateCompile) { @@ -286,62 +440,224 @@ class ParseIntent { } this.intentData.push(intentObj); } else { - throw Error(`${DECORATOR_STATEMENT_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = 'Decorator is not CallExpression'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101003', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + + private processFormInfo(node: ts.ClassDeclaration, formClassPath: string, pkgParams: object, + intentObj: object): void { + if (this.moduleJsonInfo.size === 0 && pkgParams.pkgPath) { + this.readModuleJsonInfo(pkgParams); + } + const extensionAbilities: object[] = this.moduleJsonInfo.get('extensionAbilities'); + const bindFormInfo: object = extensionAbilities.find(extensionInfo => { + const formSrcEntryPath: string = path.join(pkgParams.pkgPath, 'src', 'main', extensionInfo.srcEntry); + return formSrcEntryPath === formClassPath && extensionInfo.type === 'form'; + }); + this.verifyFormName(bindFormInfo, intentObj); + const isExported: boolean = node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword); + const isDefault: boolean = node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.DefaultKeyword); + if (!(bindFormInfo && isExported && isDefault)) { + const errorMessage: string = '@InsightIntentForm must be decorated on a formExtensionAbility'; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101006', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + + private verifyFormName(bindFormInfo: object, intentObj: object): void { + if (!bindFormInfo) { + return; + } + let formNameFound: boolean = false; + intentObj.abilityName = bindFormInfo.name; + bindFormInfo.metadata?.forEach(metaData => { + const formConfigName = `${metaData.resource.split(':').pop()}.json`; + const formConfigPath = path.join(projectConfig.aceProfilePath, formConfigName); + if (!fs.existsSync(formConfigPath)) { + return; + } + const formData = fs.readFileSync(formConfigPath, 'utf8'); + const formConfigs = JSON.parse(formData).forms; + if (formConfigs?.some(form => form.name === intentObj.formName)) { + formNameFound = true; + } + }); + if (!formNameFound) { + const errorMessage: string = '@InsightIntentForm param formName must match the card name. ' + + `Provided name: ${intentObj.formName}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101006', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + + private readModuleJsonInfo(pkgParams: object): void { + const moduleJsonPath: string = path.join(pkgParams.pkgPath, 'src', 'main', 'module.json5'); + if (fs.existsSync(moduleJsonPath)) { + const jsonStr: string = fs.readFileSync(moduleJsonPath, 'utf8'); + const obj: object = json5.parse(jsonStr); + if (obj.module?.abilities) { + this.moduleJsonInfo.set('abilities', obj.module.abilities); + } + if (obj.module?.extensionAbilities) { + this.moduleJsonInfo.set('extensionAbilities', obj.module.extensionAbilities); + } + } else { + const errorMessage: string = ` module.json5 not found, expect moduleJsonPath: ${moduleJsonPath}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101007', + description: 'ArkTs InsightIntent Error' + }); + return; } } - transformPagePath(intentObj: object, pkgParams: object): void { + private validatePagePath(intentObj: object, pkgParams: object): void { if (pkgParams.pkgPath) { - const normalPagePath: string = path.join(pkgParams.pkgPath, 'src/main', intentObj.pagePath + '.ets'); + const normalPagePath: string = path.join(pkgParams.pkgPath, 'src', 'main', intentObj.pagePath + '.ets'); if (!fs.existsSync(normalPagePath)) { - throw Error(`${INVALID_PAGEPATH_ERROR.toString()}, ${normalPagePath} does not exist, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `@InsightIntentPage pagePath incorrect,` + + `${normalPagePath} does not exist, invalidDecoratorPath: ${this.currentFilePath}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101008', + description: 'ArkTs InsightIntent Error' + }); + return; } else { const Logger: IntentLogger = IntentLogger.getInstance(); - intentObj.pagePath = '@normalized:' + getNormalizedOhmUrlByFilepath(normalPagePath, projectConfig, Logger, pkgParams, null); + intentObj.pagePath = + '@normalized:' + getNormalizedOhmUrlByFilepath(normalPagePath, projectConfig, Logger, pkgParams, null); } } } - analyzeBaseClass(node: ts.ClassDeclaration, pkgParams: object, intentObj: object): void { + private isGlobalPath(parentFilePath: string): boolean { + return globalModulePaths?.some(globalPath => { + const normalizedParent: string = path.normalize(parentFilePath).replace(/\\/g, '/'); + const normalizedGlobalPath: string = path.normalize(globalPath).replace(/\\/g, '/'); + return normalizedParent.startsWith(normalizedGlobalPath); + }); + } + + private analyzeBaseClass(node: ts.ClassDeclaration, pkgParams: object, intentObj: object, + decoratorFlag: string): void { const interfaces: ts.ExpressionWithTypeArguments[] = []; - node.heritageClauses?.forEach(clause => { - if (clause.token === ts.SyntaxKind.ImplementsKeyword || clause.token === ts.SyntaxKind.ExtendsKeyword) { - interfaces.push(...clause.types); + if (decoratorFlag === COMPONENT_USER_INTENTS_DECORATOR_ENTRY) { + node.heritageClauses?.forEach(clause => { + if (clause.token === ts.SyntaxKind.ExtendsKeyword) { + interfaces.push(...clause.types); + } + }); + this.processEntryBaseClass(interfaces, intentObj, pkgParams); + } else if (decoratorFlag === COMPONENT_USER_INTENTS_DECORATOR_ENTITY) { + node.heritageClauses?.forEach(clause => { + if (clause.token === ts.SyntaxKind.ImplementsKeyword || clause.token === ts.SyntaxKind.ExtendsKeyword) { + interfaces.push(...clause.types); + } + }); + if (interfaces.length > 0) { + const parentNode: ts.ExpressionWithTypeArguments = interfaces[0]; + this.analyzeClassHeritage(parentNode, node, pkgParams, intentObj); + } else { + const errorMessage: string = + `decorated with @InsightIntentEntity must be ultimately inherited to insightIntent.IntentEntity`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101009', + description: 'ArkTs InsightIntent Error' + }); + return; } - }); + } + } + + private processEntryBaseClass(interfaces: ts.ExpressionWithTypeArguments[], intentObj: object, + pkgParams: object): void { if (interfaces.length > 0) { const parentNode: ts.ExpressionWithTypeArguments = interfaces[0]; - if (parentNode) { - this.analyzeClassHeritage(parentNode, node, pkgParams, intentObj); + const parentClassName: string = parentNode.expression.getText(); + const parentNodeSymbol: ts.Symbol = this.checker.getTypeAtLocation(parentNode).getSymbol(); + const parentFilePath: string = parentNodeSymbol.getDeclarations()?.[0].getSourceFile().fileName; + const isGlobalPathFlag: boolean = this.isGlobalPath(parentFilePath); + if (!(isGlobalPathFlag && parentClassName === 'InsightIntentEntryExecutor')) { + const errorMessage: string = + `decorated with @InsightIntentEntry must be inherited to InsightIntentEntryExecutor`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101010', + description: 'ArkTs InsightIntent Error' + }); + return; } + const logger: IntentLogger = IntentLogger.getInstance(); + const parentRecordName: string = + getNormalizedOhmUrlByFilepath(parentFilePath, projectConfig, logger, pkgParams, null); + const recordPath: string = isGlobalPathFlag ? `sdk` : `@normalized:${parentRecordName}`; + this.collectClassInheritanceInfo(parentNode, intentObj, parentClassName, recordPath); } else { - throw Error(`${INVALID_BASE_CLASS_ERROR.toString()}, - The custom intent must be ultimately inherited to InsightIntentEntryExecutor, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `decorated with @InsightIntentEntry must be inherited to InsightIntentEntryExecutor`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101010', + description: 'ArkTs InsightIntent Error' + }); + return; } } - hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean { + private hasModifier(node: ts.Node, modifier: ts.SyntaxKind): boolean { return (node.modifiers || []).some(m => m.kind === modifier); } - parseClassMethods(classNode: ts.ClassDeclaration, decoratorType: string): methodParametersInfo[] { + private parseClassMethods(classNode: ts.ClassDeclaration, decoratorType: string): methodParametersInfo[] { const methodsArr: methodParametersInfo[] = []; for (const member of classNode.members) { if (!ts.isMethodDeclaration(member) || !this.hasModifier(member, ts.SyntaxKind.StaticKeyword)) { continue; } - const decorator: boolean = member.modifiers?.find(m => { + const decorator: ts.ModifierLike = member.modifiers?.find(m => { if (!ts.isDecorator(m)) { return false; } let decoratorName: string | undefined; if (ts.isCallExpression(m.expression)) { - decoratorName = '@' + m.expression.expression.getText(); + decoratorName = `@${decorator.expression.expression.getText()}`; } return decoratorName === decoratorType; }); if (decorator && ts.isCallExpression(decorator.expression)) { - const parameters: Record = {}; + let parameters: Record = {}; member.parameters.forEach(param => { const paramName: string = param.name.getText(); parameters[paramName] = this.checker.typeToString( @@ -361,7 +677,7 @@ class ParseIntent { return methodsArr; } - analyzeClassHeritage( + private analyzeClassHeritage( parentNode: ts.ExpressionWithTypeArguments, node: ts.ClassDeclaration, pkgParams: object, intentObj: object ): void { const parentSymbol: ts.Symbol = this.checker.getTypeAtLocation(parentNode).getSymbol(); @@ -373,29 +689,37 @@ class ParseIntent { parentClassName = parentSymbol.getName(); } }); + intentObj.parentClassName = parentClassName; const parentFilePath: string = parentSymbol.getDeclarations()?.[0].getSourceFile().fileName; const logger: IntentLogger = IntentLogger.getInstance(); - const parentRecordName: string = getNormalizedOhmUrlByFilepath(parentFilePath, projectConfig, logger, pkgParams, null); const baseClassName: string = this.checker.getTypeAtLocation(node).getSymbol().getName(); const baseFilePath: string = node.getSourceFile().fileName; const baseRecordName: string = getNormalizedOhmUrlByFilepath(baseFilePath, projectConfig, logger, pkgParams, null); - const isGlobalPath: boolean = globalModulePaths?.some(globalPath => { - const normalizedParent: string = path.normalize(parentFilePath).replace(/\\/g, '/'); - const normalizedGlobalPath: string = path.normalize(globalPath).replace(/\\/g, '/'); - return normalizedParent.startsWith(normalizedGlobalPath); - }); - const recordPath: string = isGlobalPath ? `sdk` : `@normalized:${parentRecordName}`; - if (isGlobalPath) { - if (parentClassName !== 'InsightIntentEntryExecutor') { - throw Error(`${INVALID_BASE_CLASS_ERROR.toString()}, - The custom intent must be ultimately inherited to InsightIntentEntryExecutor, invalidDecoratorPath: ${this.currentFilePath}`); + const isGlobalPathFlag: boolean = this.isGlobalPath(parentFilePath); + const parentRecordName: string = + getNormalizedOhmUrlByFilepath(parentFilePath, projectConfig, logger, pkgParams, null); + if (isGlobalPathFlag) { + if (parentClassName !== 'IntentEntity') { + const errorMessage: string = + `decorated with @InsightIntentEntity must be ultimately inherited to insightIntent.IntentEntity`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101009', + description: 'ArkTs InsightIntent Error' + }); + return; } + this.EntityHeritageClassSet.add(parentClassName + '_' + `sdk`); + } else { + this.EntityHeritageClassSet.add(parentClassName + '_' + `@normalized:${parentRecordName}`); + this.EntityExtendsMap.set(baseClassName, parentClassName); } this.heritageClassSet.add(baseClassName + '_' + `@normalized:${baseRecordName}`); - this.collectClassInheritanceInfo(parentNode, intentObj, parentClassName, recordPath); } - collectClassInheritanceInfo( + private collectClassInheritanceInfo( parentNode: ts.ExpressionWithTypeArguments, intentObj: object, parentClassName: string, recordPath: string ): void { const ClassInheritanceInfo: object = { @@ -405,7 +729,7 @@ class ParseIntent { }; if (parentNode.typeArguments?.length > 0) { parentNode.typeArguments.forEach((arg): void => { - this.getInheritanceInfoByTypeNode(arg, ClassInheritanceInfo); + this.getInheritanceInfoByTypeNode(arg, ClassInheritanceInfo, intentObj); }); } Object.assign(intentObj, { @@ -413,12 +737,12 @@ class ParseIntent { }); } - getInheritanceInfoByTypeNode(arg: ts.TypeNode, ClassInheritanceInfo: object): void { + private getInheritanceInfoByTypeNode(arg: ts.TypeNode, ClassInheritanceInfo: object, intentObj: object): void { const generic = {}; const genericType: ts.Type = this.checker.getTypeAtLocation(arg); let genericName: string; let genericSource: string | undefined; - let recordGenericSource : string; + let recordGenericSource: string; const genericSymbol: ts.Symbol | undefined = genericType.getSymbol(); if (genericSymbol) { genericName = genericSymbol.getName(); @@ -441,51 +765,111 @@ class ParseIntent { 'typeName': genericName, 'definitionFilePath': recordGenericSource }); + if (this.entityOwnerMap.has(intentObj.intentName)) { + const entityNames: string[] = this.entityOwnerMap.get(intentObj.intentName); + entityNames.push(genericName); + this.entityOwnerMap.set(intentObj.intentName, entityNames); + } else { + this.entityOwnerMap.set(intentObj.intentName, [genericName]); + } ClassInheritanceInfo.generics.push(generic); } - isPrimitiveType(type: ts.Type): boolean { + private isPrimitiveType(type: ts.Type): boolean { return ( (type.flags & ts.TypeFlags.StringLike) || - (type.flags & ts.TypeFlags.NumberLike) || - (type.flags & ts.TypeFlags.BooleanLike) + (type.flags & ts.TypeFlags.NumberLike) || + (type.flags & ts.TypeFlags.BooleanLike) ) !== 0; } - parseClassNode(node: ts.ClassDeclaration): Record { + private parseClassNode(node: ts.ClassDeclaration, intentName: string): Record { const mergedObject: Record = {}; - node.members - .filter((member): member is ts.PropertyDeclaration => { - return ts.isPropertyDeclaration(member) && - member.parent === node; - }) - .forEach(propertyNode => { - const propSymbol = this.checker.getSymbolAtLocation(propertyNode.name); - if (!propSymbol) { - return; - } - const objItem: Record = this.processProperty(propSymbol); - Object.assign(mergedObject, objItem); - }); + const type: ts.Type = this.checker.getTypeAtLocation(node); + const propertiesOfType: ts.Symbol[] = this.checker.getPropertiesOfType(type); + propertiesOfType.forEach((prop) => { + const objItem: Record = this.processProperty(prop, intentName); + Object.assign(mergedObject, objItem); + }); return mergedObject; } - processProperty(prop: ts.Symbol): Record { + private getEntityId(node: ts.ClassDeclaration): string { + let entityId: string; + const type: ts.Type = this.checker.getTypeAtLocation(node); + const propertiesOfType: ts.Symbol[] = this.checker.getPropertiesOfType(type); + propertiesOfType.forEach((prop) => { + if (prop.getName() === 'entityId') { + const declaration: ts.Declaration = prop.getDeclarations()?.[0]; + if (declaration) { + const initializer = ts.isIdentifier(declaration.initializer) ? + this.checker.getSymbolAtLocation(declaration.initializer)?.valueDeclaration?.initializer : + declaration.initializer; + entityId = initializer.text; + } + } + }); + return entityId; + } + + private processProperty(prop: ts.Symbol, intentName: string): Record { const propType: ts.Type = this.checker.getTypeOfSymbol(prop); - const {category} = this.getTypeCategory(propType); + const { category } = this.getTypeCategory(propType); const obj: Record = {}; const propName: string = prop.getName(); if (category === 'object') { - obj[propName] = 'object'; + if (this.isEntity(propType, intentName)) { + obj[propName] = 'object'; + } } else if (category === 'array') { - obj[propName] = 'array'; + if (this.isEntity(propType, intentName)) { + obj[propName] = 'array'; + } } else { obj[propName] = this.checker.typeToString(propType); } return obj; } - getTypeCategory(type: ts.Type): { category: 'array' | 'object'; } { + private isEntity(propType: ts.Type, intentName: string): boolean { + let propDeclaration: ts.Declaration; + let elementType: ts.Type | undefined; + const typeSymbol: ts.Symbol = propType.getSymbol(); + const propertyClassName: string = typeSymbol.getName(); + if (this.isArrayType(propType)) { + elementType = (propType as ts.TypeReference).typeArguments?.[0]; + propDeclaration = elementType.getSymbol().getDeclarations()[0]; + } else { + propDeclaration = typeSymbol.getDeclarations()?.[0]; + } + if (!propDeclaration) { + return false; + } + return propDeclaration.modifiers?.some(decorator => { + if (!ts.isDecorator(decorator)) { + return false; + } + let decoratorName: string | undefined; + if (ts.isCallExpression(decorator.expression)) { + decoratorName = `@${decorator.expression.expression.getText()}`; + } + if (decoratorName === '@InsightIntentEntity') { + const typeSymbol: ts.Symbol = propType.getSymbol(); + const propertyClassName: string = typeSymbol.getName(); + if (this.entityOwnerMap.has(intentName)) { + const entityNames: string[] = this.entityOwnerMap.get(intentName); + entityNames.push(propertyClassName); + this.entityOwnerMap.set(intentName, entityNames); + } else { + this.entityOwnerMap.set(intentName, [propertyClassName]); + } + return true; + } + return false; + }); + } + + private getTypeCategory(type: ts.Type): { category: 'array' | 'object'; } { const flags: ts.TypeFlags = type.getFlags(); const isPrimitive: boolean = !!(flags & ts.TypeFlags.StringLike) || @@ -494,15 +878,7 @@ class ParseIntent { !!(flags & ts.TypeFlags.Null) || !!(flags & ts.TypeFlags.Undefined); - let isArray: boolean; - const symbol: ts.Symbol | undefined = type.getSymbol(); - if (symbol) { - isArray = symbol.getName() === 'Array'; - } else { - isArray = !!(flags & ts.TypeFlags.Object) && - !!(type as ts.ObjectType).objectFlags && ts.ObjectFlags.Reference && - ((type as ts.TypeReference).target.getSymbol()?.getName() === 'Array'); - } + const isArray: boolean = this.isArrayType(type); const isObject: boolean = !isPrimitive && !isArray && !!(flags & ts.TypeFlags.Object); @@ -512,17 +888,31 @@ class ParseIntent { } else if (isObject) { category = 'object'; } - return {category}; + return { category }; + } + + private isArrayType(type: ts.Type): boolean { + let isArray: boolean; + const symbol: ts.Symbol | undefined = type.getSymbol(); + const flags: ts.TypeFlags = type.getFlags(); + if (symbol) { + isArray = symbol.getName() === 'Array'; + } else { + isArray = !!(flags & ts.TypeFlags.Object) && + !!(type as ts.ObjectType).objectFlags && ts.ObjectFlags.Reference && + ((type as ts.TypeReference).target.getSymbol()?.getName() === 'Array'); + } + return isArray; } - removeDecorator(node: ts.ClassDeclaration, decoratorNames: string[]): ts.ClassDeclaration { + private removeDecorator(node: ts.ClassDeclaration, decoratorNames: string[]): ts.ClassDeclaration { const filteredModifiers: ts.ModifierLike[] = node.modifiers.filter(decorator => { if (!ts.isDecorator(decorator)) { return true; } let decoratorName: string | undefined; if (ts.isCallExpression(decorator.expression)) { - decoratorName = '@' + decorator.expression.expression.getText(); + decoratorName = `@${decorator.expression.expression.getText()}`; } return !decoratorNames.includes(decoratorName); }); @@ -539,7 +929,7 @@ class ParseIntent { ); } - reduceMembers(member : ts.ClassElement): ts.ClassElement { + private reduceMembers(member: ts.ClassElement): ts.ClassElement { if (ts.isMethodDeclaration(member) && this.hasModifier(member, ts.SyntaxKind.StaticKeyword)) { const memberModifiers: ts.ModifierLike[] = (member.modifiers ?? []).filter(decorator => { if (!ts.isDecorator(decorator)) { @@ -547,7 +937,7 @@ class ParseIntent { } let decoratorName: string | undefined; if (ts.isCallExpression(decorator.expression)) { - decoratorName = '@' + decorator.expression.expression.getText(); + decoratorName = `@${decorator.expression.expression.getText()}`; } return decoratorName !== COMPONENT_USER_INTENTS_DECORATOR_METHOD; }); @@ -568,7 +958,7 @@ class ParseIntent { return member; } - isSymbolConstant(symbol: ts.Symbol): boolean { + private isSymbolConstant(symbol: ts.Symbol): boolean { const declaration: Declaration = symbol.valueDeclaration; if (!this.isConstVariable(declaration)) { @@ -579,7 +969,7 @@ class ParseIntent { return initializer ? this.isConstantExpression(initializer) : false; } - isConstVariable(node: ts.Node | undefined): node is ts.VariableDeclaration { + private isConstVariable(node: ts.Node | undefined): node is ts.VariableDeclaration { if (!node || !ts.isVariableDeclaration(node)) { return false; } @@ -589,9 +979,10 @@ class ParseIntent { (varList.flags & ts.NodeFlags.Const) !== 0; } - isConstantExpression(node: ts.Node): boolean { + private isConstantExpression(node: ts.Node): boolean { let flag: boolean = true; - if (ts.isLiteralExpression(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) { + if (ts.isLiteralExpression(node) || node.kind === ts.SyntaxKind.TrueKeyword || + node.kind === ts.SyntaxKind.FalseKeyword) { flag = true; } @@ -621,12 +1012,20 @@ class ParseIntent { }); } if (!flag) { - throw Error(`${DYNAMIC_PARAM_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Dynamic variable cannot be resolved`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101011', + description: 'ArkTs InsightIntent Error' + }); + return false; } return flag; } - validateRequiredIntentLinkInfo( + private validateRequiredIntentLinkInfo( node: ts.ObjectLiteralExpression, paramCheckFields: ParamChecker ): void { @@ -644,11 +1043,20 @@ class ParseIntent { } const missingFields: (keyof T)[] = requiredFields.filter(f => !existingParams.has(f)); if (missingFields.length > 0) { - throw Error(`${REQUIRED_PARAM_DISMISS_ERROR.toString()}, cause params: ${missingFields.join(', ')}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Decorator args missing required param, cause params: ${missingFields.join(', ')}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101012', + description: 'ArkTs InsightIntent Error' + }); + return; } } - validateSelfParamFields(prop: ts.Node, nestedCheckers: Map>): void { + private validateSelfParamFields(prop: ts.Node, + nestedCheckers: Map>): void { const checker: ParamChecker = nestedCheckers.get(prop.name.text); if (ts.isArrayLiteralExpression(prop.initializer)) { prop.initializer.elements.every(elem => { @@ -669,30 +1077,56 @@ class ParseIntent { } } - validateFields( + private validateFields( prop: ts.Node, allowedFields: Set, paramValidators: Record boolean> ): void { const paramName: keyof T = prop.name.text; if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { if (!allowedFields.has(paramName)) { - throw Error(`${DISALLOWED_PARAM_ERROR}, cause undeclared param: '${paramName.toString()}', invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = + `Decorator args missing required param, cause undeclared param: ${paramName.toString()}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101012', + description: 'ArkTs InsightIntent Error' + }); + return; } const validator: Function = paramValidators[paramName]; if (ts.isIdentifier(prop.initializer)) { const symbol: ts.Symbol | undefined = this.checker.getSymbolAtLocation(prop.initializer); const declaration: ts.Declaration = symbol?.valueDeclaration; if (validator && !validator(declaration?.initializer)) { - throw Error(`${INCORRECT_PARAM_TYPE_ERROR.toString()}, cause param: '${paramName.toString()}', invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Param parsing occurs error param type, cause param: ${paramName.toString()}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101013', + description: 'ArkTs InsightIntent Error' + }); + return; } } else { if (validator && !validator(prop.initializer)) { - throw Error(`${INCORRECT_PARAM_TYPE_ERROR.toString()}, cause param: '${paramName.toString()}', invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Param parsing occurs error param type, cause param: ${paramName.toString()}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101013', + description: 'ArkTs InsightIntent Error' + }); + return; } } } } - analyzeDecoratorArgs(args: ts.NodeArray, intentObj: object, paramChecker: ParamChecker): void { + private analyzeDecoratorArgs(args: ts.NodeArray, intentObj: object, + paramChecker: ParamChecker): void { args.forEach(arg => { if (ts.isIdentifier(arg)) { const symbol: ts.Symbol | undefined = this.checker.getSymbolAtLocation(arg); @@ -707,43 +1141,38 @@ class ParseIntent { }); } - createObfuscation(classNode: ts.Node): void { - ProjectCollections.projectWhiteListManager?.fileWhiteListInfo.fileKeepInfo.arkUIKeepInfo.globalNames.add(classNode.symbol.name); + private createObfuscation(classNode: ts.Node): void { + ProjectCollections.projectWhiteListManager?.fileWhiteListInfo.fileKeepInfo.arkUIKeepInfo.globalNames.add(classNode.name.text); const isExported: boolean = classNode.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword); if (isExported) { - ProjectCollections.projectWhiteListManager?.fileWhiteListInfo.fileKeepInfo.arkUIKeepInfo.propertyNames.add(classNode.symbol.name); + ProjectCollections.projectWhiteListManager?.fileWhiteListInfo.fileKeepInfo.arkUIKeepInfo.propertyNames.add(classNode.name.text); } classNode.members.forEach(member => { - if (ts.isPropertyDeclaration(member) && member.name || ts.isFunctionDeclaration(member) || ts.isMethodDeclaration(member) || - ts.isGetAccessor(member) || ts.isSetAccessor(member)) { + if (ts.isPropertyDeclaration(member) && member.name || ts.isFunctionDeclaration(member) || + ts.isMethodDeclaration(member) || + ts.isGetAccessor(member) || ts.isSetAccessor(member)) { const propName: string = member.name.getText(); ProjectCollections.projectWhiteListManager?.fileWhiteListInfo.fileKeepInfo.arkUIKeepInfo.propertyNames.add(propName); } }); } - parseStaticObject(node: ts.Node, visited: Set = new Set()): StaticValue | undefined { + private parseStaticObject(node: ts.Node, visited: Set = new Set()): StaticValue | undefined { if (visited.has(node)) { - throw Error(`${PARAM_CIRCULAR_REFERENCE_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Circular reference detected in param`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101023', + description: 'ArkTs InsightIntent Error' + }); + return undefined; } visited.add(node); - if (ts.isStringLiteral(node)) { - return node.text; - } - if (ts.isNumericLiteral(node)) { - return parseFloat(node.text); - } - if (node.kind === ts.SyntaxKind.TrueKeyword) { - return true; - } - if (node.kind === ts.SyntaxKind.FalseKeyword) { - return false; - } - if (node.kind === ts.SyntaxKind.NullKeyword) { - return null; - } - if (node.kind === ts.SyntaxKind.UndefinedKeyword) { - return undefined; + const literalValue: StaticValue | undefined = this.parseLiteralValue(node); + if (literalValue !== undefined) { + return literalValue; } if (ts.isIdentifier(node)) { const isStatic: boolean = this.isConstantExpression(node); @@ -769,10 +1198,40 @@ class ParseIntent { if (ts.isPropertyAccessExpression(node)) { return this.processEnumElement(node); } - throw Error(`${UNSUPPORTED_PARSE_ERROR.toString()}, cause param: '${node.text}', invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Unsupported parameter type cannot be parsed, cause param: ${node.text}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101014', + description: 'ArkTs InsightIntent Error' + }); + return undefined; } - processEnumElement(node: ts.PropertyAccessExpression): string { + private parseLiteralValue(node: ts.Node): StaticValue | undefined { + if (ts.isStringLiteral(node)) { + return node.text; + } + if (ts.isNumericLiteral(node)) { + return parseFloat(node.text); + } + if (node.kind === ts.SyntaxKind.TrueKeyword) { + return true; + } + if (node.kind === ts.SyntaxKind.FalseKeyword) { + return false; + } + if (node.kind === ts.SyntaxKind.NullKeyword) { + return null; + } + if (node.kind === ts.SyntaxKind.UndefinedKeyword) { + return undefined; + } + return undefined; + } + + private processEnumElement(node: ts.PropertyAccessExpression): string { const enumValue: string = node?.getText().split('.').pop(); const executeModeEnum: Map = new Map(); executeModeEnum.set('UI_ABILITY_FOREGROUND', '0'); @@ -787,11 +1246,19 @@ class ParseIntent { } else if (paramCategoryEnum.has(enumValue)) { return paramCategoryEnum.get(enumValue); } else { - throw Error(`${UNSUPPORTED_PARSE_ERROR.toString()}, cause param: '${node.text}', invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Unsupported parameter type cannot be parsed, cause param: ${node.text}`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101014', + description: 'ArkTs InsightIntent Error' + }); + return ''; } } - processObjectElements(elements: ts.ObjectLiteralExpression): { [key: string]: StaticValue } { + private processObjectElements(elements: ts.ObjectLiteralExpression): { [key: string]: StaticValue } { const obj: { [key: string]: StaticValue } = {}; for (const prop of elements.properties) { if (ts.isPropertyAssignment(prop)) { @@ -812,7 +1279,7 @@ class ParseIntent { return obj; } - processArrayElements(elements: readonly ts.Node[]): StaticValue[] { + private processArrayElements(elements: readonly ts.Node[]): StaticValue[] { const parsedElements: StaticValue[] = []; elements.forEach((element) => { @@ -830,7 +1297,7 @@ class ParseIntent { return parsedElements; } - parsePropertyKey(node: ts.PropertyName): string | undefined { + private parsePropertyKey(node: ts.PropertyName): string | undefined { if (ts.isLiteralExpression(node)) { return node.text; } @@ -841,7 +1308,7 @@ class ParseIntent { return undefined; } - processExecuteModeParam(intentObj: object): void { + private processExecuteModeParam(intentObj: object): void { if (intentObj.executeMode) { intentObj.executeMode.forEach((item: string, index: number) => { if (item === '0') { @@ -860,7 +1327,7 @@ class ParseIntent { } } - collectSchemaInfo(intentObj: object): void { + private collectSchemaInfo(intentObj: object): void { if (intentObj.schema) { const schemaPath: string = path.join( __dirname, 'schema', @@ -869,29 +1336,34 @@ class ParseIntent { if (fs.existsSync(schemaPath)) { const schemaContent: string = fs.readFileSync(schemaPath, 'utf-8'); const schemaObj: object = JSON.parse(schemaContent); - intentObj.parameters = schemaObj.params; + intentObj.parameters = schemaObj.parameters; intentObj.llmDescription = schemaObj.llmDescription; intentObj.keywords = schemaObj.keywords; intentObj.intentName = schemaObj.intentName; + intentObj.result = schemaObj.result; + intentObj.example = schemaObj.example; } } } - verifyInheritanceChain(): void { - this.intentData.forEach((element): void => { - if (element.ClassInheritanceInfo) { - const parentClassName: string = element.ClassInheritanceInfo.parentClassName; - const definitionFilePath: string = element.ClassInheritanceInfo.definitionFilePath; - const verifiedString: string = parentClassName + '_' + definitionFilePath; - if (!this.heritageClassSet.has(verifiedString)) { - throw Error(`${INVALID_BASE_CLASS_ERROR.toString()} - , Subclass must inherit from a class decorated with @InsightIntentEntry, invalidDecoratorPath: ${this.currentFilePath}\``); - } + private verifyInheritanceChain(): void { + this.EntityHeritageClassSet.forEach(entityClassInfo => { + if (!this.heritageClassSet.has(entityClassInfo)) { + const errorMessage: string = + `decorated with @InsightIntentEntity must be ultimately inherited to insightIntent.IntentEntity`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101009', + description: 'ArkTs InsightIntent Error' + }); + return; } }); } - schemaValidationRequiredRule(schemaData: Record, schemaObj: object): void { + private schemaValidationRequiredRule(schemaData: Record, schemaObj: object): void { const reqData: Map = new Map(); schemaObj.required.forEach(key => reqData.set(key, true)); if (schemaObj.properties) { @@ -899,17 +1371,33 @@ class ParseIntent { const keyArr: string[] = Object.keys(paramsSchema); keyArr.forEach(key => { if (!schemaData[key] && reqData.get(key)) { - throw Error(`${SCHEMA_VALIDATE_REQUIRED_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Schema verification required parameter does not exist`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101016', + description: 'ArkTs InsightIntent Error' + }); + return; } }); } } - schemaPropertiesValidation(schemaData: Record, schemaObj: object): void { + private schemaPropertiesValidation(schemaData: Record, schemaObj: object): void { if (schemaObj.properties) { Object.entries(schemaObj.properties).forEach(([key, value]) => { if (schemaData[key] && value.type !== schemaData[key]) { - throw Error(`${SCHEMA_VALIDATE_TYPE_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Schema verification parameter type error`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101017', + description: 'ArkTs InsightIntent Error' + }); + return; } }); } @@ -929,7 +1417,15 @@ class ParseIntent { } }); if (count !== 1) { - throw Error(`${SCHEMA_VALIDATE_ONE_OF_ERROR.toString()} , invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Not meeting the one of schema verification rules`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101024', + description: 'ArkTs InsightIntent Error' + }); + return; } } if (schemaObj.anyOf) { @@ -944,12 +1440,20 @@ class ParseIntent { } }); if (count === 0) { - throw Error(`${SCHEMA_VALIDATE_ANY_OF_ERROR.toString()} , invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Not meeting the any of schema verification rules`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101018', + description: 'ArkTs InsightIntent Error' + }); + return; } } } - schemaValidateSync(schemaData: Record, schemaObj: object): void { + private schemaValidateSync(schemaData: Record, schemaObj: object): void { if (!schemaObj) { return; } @@ -960,7 +1464,15 @@ class ParseIntent { this.schemaValidateSync(schemaData, schemaObj.items.items); } if (schemaObj.type !== 'object') { - throw Error(`${SCHEMA_ROOT_TYPE_MISMATCH_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Schema root type must be object`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101019', + description: 'ArkTs InsightIntent Error' + }); + return; } if (schemaObj.properties) { const items: string[] = Object.keys(schemaObj.properties); @@ -976,24 +1488,125 @@ class ParseIntent { this.schemaValidateRules(schemaData, schemaObj); } - schemaAdditionalPropertiesValidation(schemaData, schemaProps): void { + private schemaAdditionalPropertiesValidation(schemaData, schemaProps): void { for (const key of Object.keys(schemaData)) { if (!schemaProps[key]) { - throw Error(`${SCHEMA_VALIDATE_TYPE_ERROR.toString()}, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Schema does not allow more parameters`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101025', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + } + + private processEntityOwnerMap(): void { + for (const [intentName, entityClassNames] of this.entityOwnerMap.entries()) { + const expandedClassNames = new Set(entityClassNames); + entityClassNames.forEach(className => { + this.visitEntityHeritage(className, expandedClassNames); + }); + if (expandedClassNames.size > entityClassNames.length) { + this.entityOwnerMap.set(intentName, Array.from(expandedClassNames)); } } } - writeUserIntentJsonFile(): void { + private visitEntityHeritage(className: string, expandedClassNames: Set): void { + const parentClassName: string = this.EntityExtendsMap.get(className); + if (parentClassName && !expandedClassNames.has(parentClassName)) { + expandedClassNames.add(parentClassName); + this.visitEntityHeritage(parentClassName, expandedClassNames); + } + } + + private matchEntities(): void { + if (this.entityMap.size === 0) { + return; + } + this.processEntityOwnerMap(); + const intentNameMappingMap = new Map(); + this.intentData.forEach(data => { + intentNameMappingMap.set(data.intentName, data); + }); + for (const [intentName, entityIds] of this.entityOwnerMap.entries()) { + const targetIntent: object = intentNameMappingMap.get(intentName); + if (!targetIntent) { + continue; + } + const matchedEntities: object[] = []; + entityIds.forEach(id => { + if (this.entityMap.has(id)) { + matchedEntities.push(this.entityMap.get(id)); + } + }); + if (matchedEntities.length !== 0) { + targetIntent.entities = matchedEntities; + } + } + } + + public writeUserIntentJsonFile(): void { + this.verifyInheritanceChain(); + const writeJsonData: object = this.processIntentData(); + const cacheSourceMapPath: string = + path.join(projectConfig.aceProfilePath, 'insight_intent.json'); // The user's intents configuration file + const cachePath: string = path.join(projectConfig.cachePath, 'insight_compile_cache.json'); // Compiled cache file + try { + if (this.intentData.length > 0) { + fs.writeFileSync(cacheSourceMapPath, JSON.stringify(writeJsonData, null, 2), 'utf-8'); + fs.writeFileSync(cachePath, JSON.stringify({ 'extractInsightIntents': this.intentData }, null, 2), 'utf-8'); + } else if (fs.existsSync(cacheSourceMapPath)) { + fs.unlinkSync(cacheSourceMapPath); + } + const normalizedPath: string = path.normalize(projectConfig.aceProfilePath); + const fullPath: string = path.join(normalizedPath, '../../../module.json'); + if (fs.existsSync(fullPath)) { + const rawData: string = fs.readFileSync(fullPath, 'utf8'); + const jsonData: object = JSON.parse(rawData); + if (jsonData?.module) { + jsonData.module.hasInsightIntent = this.intentData.length > 0 ? true : undefined; + } + const updatedJson: string = JSON.stringify(jsonData, null, 2); + fs.writeFileSync(fullPath, updatedJson, 'utf8'); + } + } catch (e) { + const errorMessage: string = `Internal error writeFile error`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101020', + description: 'ArkTs InsightIntent Error' + }); + return; + } + } + + private processIntentData(): object { + this.matchEntities(); if (!projectConfig.aceProfilePath) { - throw Error(`${INTERNAL_ERROR.toString()}, aceProfilePath not found, invalidDecoratorPath: ${this.currentFilePath}`); + const errorMessage: string = `Internal error aceProfilePath not found`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101020', + description: 'ArkTs InsightIntent Error' + }); + return null; } - const cacheSourceMapPath: string = path.join(projectConfig.aceProfilePath, 'insight_intent.json'); - const cachePath: string = path.join(projectConfig.cachePath, 'insight_compile_cache.json'); + const cacheSourceMapPath: string = + path.join(projectConfig.aceProfilePath, 'insight_intent.json'); // The user's intents configuration file + const cachePath: string = path.join(projectConfig.cachePath, 'insight_compile_cache.json'); // Compiled cache file if (!fs.existsSync(projectConfig.aceProfilePath)) { - fs.mkdirSync(projectConfig.aceProfilePath, {recursive: true}); + fs.mkdirSync(projectConfig.aceProfilePath, { recursive: true }); } - if (this.isUpdateCompile) { + if (this.isUpdateCompile && fs.existsSync(cachePath)) { const cacheData: string = fs.readFileSync(cachePath, 'utf8'); const cacheDataObj: object = JSON.parse(cacheData); const insightIntents: object[] = cacheDataObj.extractInsightIntents.filter(insightIntent => { @@ -1018,48 +1631,46 @@ class ParseIntent { }); } this.validateIntentIntentName(writeJsonData); - if (this.intentData.length > 0) { - fs.writeFileSync(cacheSourceMapPath, JSON.stringify(writeJsonData, null, 2), 'utf-8'); - fs.writeFileSync(cachePath, JSON.stringify({'extractInsightIntents': this.intentData}, null, 2), 'utf-8'); - } else if (fs.existsSync(cacheSourceMapPath)) { - fs.unlinkSync(cacheSourceMapPath); - } - const normalizedPath: string = path.normalize(projectConfig.aceProfilePath); - const fullPath: string = path.join(normalizedPath, '../../../module.json'); - if (fs.existsSync(fullPath)) { - const rawData: string = fs.readFileSync(fullPath, 'utf8'); - const jsonData: object = JSON.parse(rawData); - if (jsonData?.module) { - jsonData.module.hasInsightIntent = this.intentData.length > 0 ? true : undefined; - } - const updatedJson: string = JSON.stringify(jsonData, null, 2); - fs.writeFileSync(fullPath, updatedJson, 'utf8'); - } + return writeJsonData; } - validateIntentIntentName(writeJsonData: object): void { + private validateIntentIntentName(writeJsonData: object): void { const duplicates = new Set(); writeJsonData.insightIntents?.forEach(insightIntent => { duplicates.add(insightIntent.intentName); }); writeJsonData.extractInsightIntents.forEach(item => { if (duplicates.has(item.intentName)) { - throw Error(`${DECORATOR_DUPLICATE_INTENTNAME.toString()}, value : ${item.intentName}`); - } else { + const errorMessage: string = `User intents has duplicate intentName param`; + this.transformLog.push({ + type: LogType.ERROR, + message: errorMessage, + pos: this.currentNode.getStart(), + code: '10101021', + description: 'ArkTs InsightIntent Error' + }); + return; + } else if (item.intentName !== undefined) { duplicates.add(item.intentName); } }); } - clear(): void { + public clear(): void { this.intentData = []; this.checker = null; this.currentFilePath = ''; this.heritageClassSet = new Set(); + this.heritageClassSet.add('IntentEntity_sdk'); this.heritageClassSet.add('InsightIntentEntryExecutor_sdk'); this.isInitCache = false; - this.isUpdateCompile = false; + this.isUpdateCompile = true; this.updatePageIntentObj = new Map(); + this.entityMap = new Map(); + this.entityOwnerMap = new Map(); + this.moduleJsonInfo = new Map(); + this.EntityHeritageClassSet = new Set(); + this.EntityExtendsMap = new Map(); } } diff --git a/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.js.sample b/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.js.sample new file mode 100644 index 000000000..1896d0476 --- /dev/null +++ b/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.js.sample @@ -0,0 +1,26 @@ +/* + * 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. + */ + +let __generate__Id = 0; +function generateId() { + return "@InsightIntentEntity_" + ++__generate__Id; +} +export class ArtistClassDef { + constructor() { + this.entityId = "id"; + this.name = ''; + } +} +//# sourceMappingURL=@InsightIntentEntity.js.map \ No newline at end of file diff --git a/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.js.sample b/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.js.sample new file mode 100644 index 000000000..53c9c02e0 --- /dev/null +++ b/compiler/test/transform_ut/application/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.js.sample @@ -0,0 +1,49 @@ +/* + * 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. + */ + +let __generate__Id = 0; +function generateId() { + return "@InsightIntentForm_" + ++__generate__Id; +} +export default class EntryFormAbilityaa extends FormExtensionAbility { + constructor() { + super(...arguments); + this.songName = ""; + } + onAddForm(want) { + // Called to return a FormBindingData object. + let formData = ''; + return formBindingData.createFormBindingData(formData); + } + onCastToNormalForm(formId) { + // Called when the form provider is notified that a temporary form is successfully + // converted to a normal form. + } + onUpdateForm(formId) { + // Called to notify the form provider to update a specified form. + } + onFormEvent(formId, message) { + // Called when a specified message event defined by the form provider is triggered. + } + onRemoveForm(formId) { + // Called to notify the form provider that a specified form has been destroyed. + } + onAcquireFormState(want) { + // Called to return a {@link FormState} object. + return formInfo.FormState.READY; + } +} +; +//# sourceMappingURL=@InsightIntentForm.js.map \ No newline at end of file diff --git a/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.ets b/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.ets new file mode 100644 index 000000000..b6184f987 --- /dev/null +++ b/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentEntity/@InsightIntentEntity.ets @@ -0,0 +1,44 @@ +/* + * 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. + */ + +@InsightIntentEntity({ + entityCategory: 'artist entity category', + parameters: { + "$id": "/schemas/ArtistClassDef", + "type": "object", + "description": "Information about the artist", + "properties": { + "country": { + "type": "string", + "description": "The artist's country of origin", + "default": "zh" + }, + "city": { + "type": "string", + "description": "The artist's city of origin" + }, + "name": { + "type": "string", + "description": "The name of the artist", + "minLength": 1 + } + }, + "required": ["name"] + } +}) +export class ArtistClassDef implements insightIntent.IntentEntity { + entityId: string = "id"; + name: string = '' +} \ No newline at end of file diff --git a/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.ets b/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.ets new file mode 100644 index 000000000..77e236242 --- /dev/null +++ b/compiler/test/transform_ut/application/entry/src/main/ets/pages/utForInsightDecorator/@InsightIntentForm/@InsightIntentForm.ets @@ -0,0 +1,92 @@ +/* + * 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. + */ + +@InsightIntentForm({ + intentName: 'PlayMusic78', + domain: 'MusicDomain', + intentVersion: '1.0.1', + displayName: '播放歌曲', + displayDescription: '播放音乐意图', + icon: "", + llmDescription: '支持传递歌曲名称,播放音乐', + keywords: ['音乐播放', '播放歌曲', 'PlayMusic'], + parameters: { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Song Schema", + "description": "A schema for describing songs and their artists", + "properties": { + "songName": { + "type": "string", + "description": "The name of the song", + "minLength": 1 + }, + "artist": { + "type": "object", + "description": "Information about the artist", + "properties": { + "country": { + "type": "string", + "description": "The artist's country of origin", + "default": "zh" + }, + "city": { + "type": "object", + "description": "The artist's city of origin" + }, + "name": { + "type": "string", + "description": "The name of the artist", + "minLength": 1 + } + }, + "required": ["name"] + } + }, + "required": ["songName"] + }, + formName: 'widget' +}) +export default class EntryFormAbilityaa extends FormExtensionAbility { + songName: string = "" + + onAddForm(want: Want) { + // Called to return a FormBindingData object. + let formData = ''; + return formBindingData.createFormBindingData(formData); + } + + onCastToNormalForm(formId: string) { + // Called when the form provider is notified that a temporary form is successfully + // converted to a normal form. + } + + onUpdateForm(formId: string) { + // Called to notify the form provider to update a specified form. + } + + onFormEvent(formId: string, message: string) { + // Called when a specified message event defined by the form provider is triggered. + } + + onRemoveForm(formId: string) { + // Called to notify the form provider that a specified form has been destroyed. + } + + onAcquireFormState(want: Want) { + // Called to return a {@link FormState} object. + return formInfo.FormState.READY; + } +}; diff --git a/compiler/test/transform_ut/helpers/insightUTPath.ts b/compiler/test/transform_ut/helpers/insightUTPath.ts index 2b5cba81d..3e5826abc 100644 --- a/compiler/test/transform_ut/helpers/insightUTPath.ts +++ b/compiler/test/transform_ut/helpers/insightUTPath.ts @@ -17,7 +17,9 @@ export const UT_PAGES: string[] = [ '@InsightIntentLink/@InsightIntentLink', '@InsightIntentEntry/@InsightIntentEntry', '@InsightIntentFunction/@InsightIntentFunction', - '@InsightIntentPage/@InsightIntentPage' + '@InsightIntentPage/@InsightIntentPage', + '@InsightIntentForm/@InsightIntentForm', + '@InsightIntentEntity/@InsightIntentEntity' ] export const CACHE_PATH: string = 'default/cache/default/default@CompileArkTS/esmodule/debug'; \ No newline at end of file -- Gitee