@cloudcare/hvigor-ohos-plugin 0.1.1 → 0.1.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,10 @@
7
7
  * never changed.
8
8
  */
9
9
 
10
+ import { hasRuntimeDependency, SourceReader } from './ft-transform-context';
11
+ import { createLexicalResolver } from './ft-lexical-scope';
12
+ import { allocateName, collectIdentifierNames } from './ft-generated-names';
13
+
10
14
  type TypeScriptApi = any;
11
15
 
12
16
  interface TransformCandidate {
@@ -17,11 +21,14 @@ interface TransformCandidate {
17
21
  level: string;
18
22
  source: 'console' | 'hilog';
19
23
  callArguments: SourceRange[];
24
+ needsStatementSeparator: boolean;
25
+ generatedNames?: string[];
20
26
  }
21
27
 
22
28
  interface SourceRange {
23
29
  start: number;
24
30
  end: number;
31
+ spread: boolean;
25
32
  }
26
33
 
27
34
  const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
@@ -45,77 +52,58 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
45
52
  ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
46
53
  }
47
54
 
48
- function collectIdentifierNames(ts: TypeScriptApi, sourceFile: any): Set<string> {
49
- const identifiers = new Set<string>();
50
- visit(ts, sourceFile, (node: any) => {
51
- if (ts.isIdentifier(node)) {
52
- identifiers.add(node.text);
53
- }
54
- });
55
- return identifiers;
56
- }
57
-
58
- function isNameDeclaredOutsideImports(ts: TypeScriptApi, sourceFile: any, name: string): boolean {
59
- let declared = false;
60
- visit(ts, sourceFile, (node: any) => {
61
- if (declared || ts.isImportDeclaration(node)) {
62
- return;
63
- }
64
- if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) ||
65
- ts.isParameter(node) || ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node) ||
66
- ts.isTypeAliasDeclaration(node)) {
67
- const declarationName = node.name;
68
- if (declarationName && ts.isIdentifier(declarationName) && declarationName.text === name) {
69
- declared = true;
70
- }
71
- }
72
- });
73
- return declared;
74
- }
75
-
76
- function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Set<string> {
77
- const bindings = new Set<string>();
55
+ function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
56
+ const bindings = new Map<string, any>();
78
57
  sourceFile.statements.forEach((statement: any) => {
79
58
  if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
80
59
  !ts.isStringLiteral(statement.moduleSpecifier) ||
81
- !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
60
+ !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
82
61
  return;
83
62
  }
84
63
 
85
64
  const importClause = statement.importClause;
86
65
  if (importClause.name) {
87
- bindings.add(importClause.name.text);
66
+ bindings.set(importClause.name.text, importClause);
88
67
  }
89
68
  if (!importClause.namedBindings) {
90
69
  return;
91
70
  }
92
71
  if (ts.isNamespaceImport(importClause.namedBindings)) {
93
- bindings.add(importClause.namedBindings.name.text);
72
+ bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
94
73
  return;
95
74
  }
96
75
  importClause.namedBindings.elements.forEach((element: any) => {
97
76
  const importedName = element.propertyName ? element.propertyName.text : element.name.text;
98
- if (importedName === 'hilog') {
99
- bindings.add(element.name.text);
77
+ if (importedName === 'hilog' && !element.isTypeOnly) {
78
+ bindings.set(element.name.text, element);
100
79
  }
101
80
  });
102
81
  });
103
82
  return bindings;
104
83
  }
105
84
 
85
+ function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
86
+ const statement = node.parent;
87
+ if (!ts.isExpressionStatement(statement)) {
88
+ return false;
89
+ }
90
+ const container = statement.parent;
91
+ // Only statement lists accept a leading empty statement. In an unbraced
92
+ // if/loop body it would replace the body; inside an expression it is invalid.
93
+ return ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) ||
94
+ ts.isCaseClause(container) || ts.isDefaultClause(container);
95
+ }
96
+
106
97
  function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): TransformCandidate[] {
107
98
  const candidates: TransformCandidate[] = [];
108
99
  const hiLogBindings = collectHiLogBindings(ts, sourceFile);
109
- const consoleIsShadowed = isNameDeclaredOutsideImports(ts, sourceFile, 'console');
110
- const shadowedHiLogBindings = new Set<string>();
111
- hiLogBindings.forEach((binding: string) => {
112
- if (isNameDeclaredOutsideImports(ts, sourceFile, binding)) {
113
- shadowedHiLogBindings.add(binding);
114
- }
115
- });
100
+ const resolve = createLexicalResolver(ts, sourceFile);
116
101
 
117
102
  visit(ts, sourceFile, (node: any) => {
118
- if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
103
+ // Retain optional calls verbatim so their receiver/method short-circuit also
104
+ // controls argument evaluation. Nested ordinary calls remain eligible.
105
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
106
+ (ts.isOptionalChain && ts.isOptionalChain(node))) {
119
107
  return;
120
108
  }
121
109
  const delegate = node.expression.expression;
@@ -133,29 +121,27 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
133
121
 
134
122
  const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
135
123
  start: argument.getStart(sourceFile),
136
- end: argument.end
124
+ end: argument.end,
125
+ spread: ts.isSpreadElement(argument)
137
126
  }));
138
- if (delegateName === 'console' && !consoleIsShadowed && CONSOLE_LEVELS.includes(level)) {
139
- candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments });
127
+ const statementSeparator = needsStatementSeparator(ts, node);
128
+ if (delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level)) {
129
+ candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
130
+ needsStatementSeparator: statementSeparator });
140
131
  return;
141
132
  }
142
- if (hiLogBindings.has(delegateName) && !shadowedHiLogBindings.has(delegateName) &&
143
- HILOG_LEVELS.includes(level) && callArguments.length >= 3) {
144
- candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments });
133
+ if (hiLogBindings.has(delegateName) && resolve(delegate, delegateName) === hiLogBindings.get(delegateName) &&
134
+ HILOG_LEVELS.includes(level) && (callArguments.length >= 3 || callArguments.some(argument => argument.spread))) {
135
+ candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
136
+ needsStatementSeparator: statementSeparator });
145
137
  }
146
138
  });
147
139
  return candidates;
148
140
  }
149
141
 
150
- function selectBridgeName(identifiers: Set<string>): string {
151
- const baseName = '__ftConsoleLogBridge';
152
- let bridgeName = baseName;
153
- let suffix = 1;
154
- while (identifiers.has(bridgeName)) {
155
- bridgeName = `${baseName}${suffix}`;
156
- suffix++;
157
- }
158
- return bridgeName;
142
+ function allocateCandidateNames(candidate: TransformCandidate, identifiers: Set<string>): string[] {
143
+ const prefix = candidate.source === 'console' ? '__ftConsole' : '__ftHiLog';
144
+ return ['Call', 'Args', 'Values'].map(suffix => allocateName(identifiers, prefix + suffix));
159
145
  }
160
146
 
161
147
  function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
@@ -187,42 +173,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
187
173
  candidate: TransformCandidate): string {
188
174
  const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
189
175
  renderCandidates(code, candidates, bridgeName, argument.start, argument.end));
176
+ const statementSeparator = candidate.needsStatementSeparator ? ';' : '';
177
+ const [original, args, values] = candidate.generatedNames!;
178
+ const valueType = '(Object | null | undefined)';
179
+ const originalType = candidate.source === 'console' ? `(...${args}: ${valueType}[]) => void` :
180
+ `(domain: number, tag: string, format: string, ...${values}: ${valueType}[]) => void`;
181
+ let body: string;
190
182
  if (candidate.source === 'console') {
191
- const argumentNames = renderedArguments.map((_argument: string, index: number) => `__ftConsoleArg${index}`);
192
- const parameterDeclarations = argumentNames.map((name: string) => `${name}: Object`).join(', ');
193
- const originalCallArguments = argumentNames.map((name: string) => `${name} as string`);
194
- const originalCall = `${candidate.delegate}.${candidate.level}(${originalCallArguments.join(', ')})`;
195
- const captureArguments = argumentNames.length > 0 ? `, ${argumentNames.join(', ')}` : '';
196
- return `((${parameterDeclarations}): void => ${bridgeName}.console('${candidate.level}', ` +
197
- `(): void => ${originalCall}${captureArguments}))(${renderedArguments.join(', ')})`;
183
+ body = `${bridgeName}.console('${candidate.level}', (): void => ${original}(...${args}), ` +
184
+ `...(${args} as Object[]))`;
185
+ } else {
186
+ const header = `${args}[0] as number, ${args}[1] as string, ${args}[2] as string`;
187
+ body = `{ const ${values}: ${valueType}[] = ${args}.slice(3); ` +
188
+ `${bridgeName}.hilog('${candidate.level}', ${header}, (): void => ${original}(${header}, ...${values}), ` +
189
+ `...(${values} as Object[])); }`;
198
190
  }
199
-
200
- const argumentNames = renderedArguments.map((_argument: string, index: number) => {
201
- if (index === 0) {
202
- return '__ftHiLogDomain';
203
- }
204
- if (index === 1) {
205
- return '__ftHiLogTag';
206
- }
207
- if (index === 2) {
208
- return '__ftHiLogFormat';
209
- }
210
- return `__ftHiLogArg${index - 3}`;
211
- });
212
- const parameterDeclarations = argumentNames.map((name: string, index: number) => {
213
- if (index === 0) {
214
- return `${name}: number`;
215
- }
216
- if (index === 1 || index === 2) {
217
- return `${name}: string`;
218
- }
219
- return `${name}: Object`;
220
- }).join(', ');
221
- const originalCall = `${candidate.delegate}.${candidate.level}(${argumentNames.join(', ')})`;
222
- const captureArguments = argumentNames.length > 3 ? `, ${argumentNames.slice(3).join(', ')}` : '';
223
- return `((${parameterDeclarations}): void => ${bridgeName}.hilog('${candidate.level}', ` +
224
- `${argumentNames[0]}, ${argumentNames[1]}, ${argumentNames[2]}, (): void => ${originalCall}` +
225
- `${captureArguments}))(${renderedArguments.join(', ')})`;
191
+ // Resolve and bind the original method before evaluating its arguments.
192
+ // Casting only the capture array keeps compatibility with older runtime HARs;
193
+ // null/undefined values themselves are forwarded unchanged.
194
+ // HiLog is a platform namespace, not an ArkTS object receiver. Its exported
195
+ // functions are static; console methods retain their receiver through bind.
196
+ const callee = `${candidate.delegate}.${candidate.level}`;
197
+ const captured = candidate.source === 'console' ? `${callee}.bind(${candidate.delegate})` : callee;
198
+ const argumentsText = renderedArguments.length > 0 ? ', ' + renderedArguments.join(', ') : '';
199
+ return `${statementSeparator}((${original}: ${originalType}, ...${args}: ${valueType}[]): void => ${body})` +
200
+ `(${captured} as ${originalType}${argumentsText})`;
226
201
  }
227
202
 
228
203
  function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
@@ -235,8 +210,9 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
235
210
  return offset;
236
211
  }
237
212
 
238
- export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string): string {
239
- if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER)) {
213
+ export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader): string {
214
+ if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
215
+ !hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
240
216
  return code;
241
217
  }
242
218
  const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
@@ -245,7 +221,9 @@ export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: s
245
221
  return code;
246
222
  }
247
223
 
248
- const bridgeName = selectBridgeName(collectIdentifierNames(ts, sourceFile));
224
+ const identifiers = collectIdentifierNames(ts, sourceFile);
225
+ const bridgeName = allocateName(identifiers, '__ftConsoleLogBridge');
226
+ candidates.forEach(candidate => { candidate.generatedNames = allocateCandidateNames(candidate, identifiers); });
249
227
  const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
250
228
  const insertionOffset = findImportInsertionOffset(ts, sourceFile);
251
229
  const transformedInsertionOffset = renderCandidates(code, candidates, bridgeName, 0, insertionOffset).length;
@@ -0,0 +1,19 @@
1
+ /** Reserve generated identifiers across all scopes and every nested rewrite. */
2
+ export function allocateName(identifiers: Set<string>, base: string): string {
3
+ let candidate = base;
4
+ let suffix = 1;
5
+ while (identifiers.has(candidate)) candidate = `${base}${suffix++}`;
6
+ identifiers.add(candidate);
7
+ return candidate;
8
+ }
9
+
10
+ export function collectIdentifierNames(ts: any, source: any): Set<string> {
11
+ const identifiers = new Set<string>();
12
+ const visit = (node: any): void => {
13
+ if (ts.isIdentifier(node)) identifiers.add(node.text);
14
+ if (node.name && ts.isStringLiteral(node.name)) identifiers.add(node.name.text);
15
+ ts.forEachChild(node, visit);
16
+ };
17
+ visit(source);
18
+ return identifiers;
19
+ }
@@ -0,0 +1,65 @@
1
+ /** Value bindings visible at a call site, including declarations later in the same scope. */
2
+ interface Scope {
3
+ parent?: Scope;
4
+ functionScope: boolean;
5
+ bindings: Map<string, any>;
6
+ }
7
+
8
+ export function createLexicalResolver(ts: any, source: any): (location: any, name: string) => any | undefined {
9
+ const scopes = new WeakMap<object, Scope>();
10
+ const root: Scope = { functionScope: true, bindings: new Map() };
11
+ const bind = (scope: Scope, name: any, declaration: any): void => {
12
+ if (!name) return;
13
+ if (ts.isIdentifier(name)) scope.bindings.set(name.text, declaration);
14
+ else if (name.elements) name.elements.forEach((element: any) => {
15
+ if (ts.isBindingElement(element)) bind(scope, element.name, declaration);
16
+ });
17
+ };
18
+ const walk = (node: any, outer: Scope): void => {
19
+ const struct = typeof ts.isStructDeclaration === 'function' && ts.isStructDeclaration(node);
20
+ const classLike = ts.isClassDeclaration(node) || ts.isClassExpression(node) || struct;
21
+ if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || struct || ts.isEnumDeclaration(node) ||
22
+ ts.isModuleDeclaration(node)) && node.name) {
23
+ bind(outer, node.name, node);
24
+ }
25
+ let scope = outer;
26
+ const functionLike = ts.isFunctionLike(node);
27
+ if (functionLike || ts.isBlock(node) || ts.isCaseBlock(node) || ts.isCatchClause(node) ||
28
+ ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node) ||
29
+ ts.isModuleBlock(node) || classLike) {
30
+ scope = { parent: outer, functionScope: functionLike || ts.isModuleBlock(node), bindings: new Map() };
31
+ }
32
+ scopes.set(node, scope);
33
+ if (functionLike) {
34
+ if (ts.isFunctionExpression(node) && node.name) bind(scope, node.name, node);
35
+ node.parameters.forEach((parameter: any) => bind(scope, parameter.name, parameter));
36
+ }
37
+ if (classLike && node.name) bind(scope, node.name, node);
38
+ if (ts.isVariableDeclaration(node)) {
39
+ const list = node.parent;
40
+ let target = scope;
41
+ if (ts.isVariableDeclarationList(list) && !(list.flags & ts.NodeFlags.BlockScoped)) {
42
+ while (!target.functionScope && target.parent) target = target.parent;
43
+ }
44
+ bind(target, node.name, node);
45
+ }
46
+ if (ts.isImportDeclaration(node) && node.importClause && !node.importClause.isTypeOnly) {
47
+ const clause = node.importClause;
48
+ if (clause.name) bind(scope, clause.name, clause);
49
+ const bindings = clause.namedBindings;
50
+ if (bindings && ts.isNamespaceImport(bindings)) bind(scope, bindings.name, bindings);
51
+ else if (bindings) bindings.elements.forEach((element: any) => {
52
+ if (!element.isTypeOnly) bind(scope, element.name, element);
53
+ });
54
+ }
55
+ if (ts.isImportEqualsDeclaration(node) && !node.isTypeOnly) bind(scope, node.name, node);
56
+ ts.forEachChild(node, (child: any) => walk(child, scope));
57
+ };
58
+ walk(source, root);
59
+ return (location: any, name: string): any | undefined => {
60
+ for (let scope = scopes.get(location); scope; scope = scope.parent) {
61
+ if (scope.bindings.has(name)) return scope.bindings.get(name);
62
+ }
63
+ return undefined;
64
+ };
65
+ }