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

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,11 @@
7
7
  * never changed.
8
8
  */
9
9
 
10
+ import { hasRuntimeDependency, SourceReader, SourceDiagnosticReporter, parseApplicationSource,
11
+ reportSourceDiagnostic, reportSourceParseError } from './ft-transform-context';
12
+ import { createLexicalResolver } from './ft-lexical-scope';
13
+ import { allocateName, collectIdentifierNames } from './ft-generated-names';
14
+
10
15
  type TypeScriptApi = any;
11
16
 
12
17
  interface TransformCandidate {
@@ -17,11 +22,14 @@ interface TransformCandidate {
17
22
  level: string;
18
23
  source: 'console' | 'hilog';
19
24
  callArguments: SourceRange[];
25
+ needsStatementSeparator: boolean;
26
+ generatedNames?: string[];
20
27
  }
21
28
 
22
29
  interface SourceRange {
23
30
  start: number;
24
31
  end: number;
32
+ spread: boolean;
25
33
  }
26
34
 
27
35
  const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
@@ -45,74 +53,53 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
45
53
  ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
46
54
  }
47
55
 
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>();
56
+ function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
57
+ const bindings = new Map<string, any>();
78
58
  sourceFile.statements.forEach((statement: any) => {
79
59
  if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
80
60
  !ts.isStringLiteral(statement.moduleSpecifier) ||
81
- !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
61
+ !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
82
62
  return;
83
63
  }
84
64
 
85
65
  const importClause = statement.importClause;
86
66
  if (importClause.name) {
87
- bindings.add(importClause.name.text);
67
+ bindings.set(importClause.name.text, importClause);
88
68
  }
89
69
  if (!importClause.namedBindings) {
90
70
  return;
91
71
  }
92
72
  if (ts.isNamespaceImport(importClause.namedBindings)) {
93
- bindings.add(importClause.namedBindings.name.text);
73
+ bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
94
74
  return;
95
75
  }
96
76
  importClause.namedBindings.elements.forEach((element: any) => {
97
77
  const importedName = element.propertyName ? element.propertyName.text : element.name.text;
98
- if (importedName === 'hilog') {
99
- bindings.add(element.name.text);
78
+ if (importedName === 'hilog' && !element.isTypeOnly) {
79
+ bindings.set(element.name.text, element);
100
80
  }
101
81
  });
102
82
  });
103
83
  return bindings;
104
84
  }
105
85
 
106
- function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): TransformCandidate[] {
86
+ function needsStatementSeparator(ts: TypeScriptApi, node: any): boolean {
87
+ const statement = node.parent;
88
+ if (!ts.isExpressionStatement(statement)) {
89
+ return false;
90
+ }
91
+ const container = statement.parent;
92
+ // Only statement lists accept a leading empty statement. In an unbraced
93
+ // if/loop body it would replace the body; inside an expression it is invalid.
94
+ return ts.isSourceFile(container) || ts.isBlock(container) || ts.isModuleBlock(container) ||
95
+ ts.isCaseClause(container) || ts.isDefaultClause(container);
96
+ }
97
+
98
+ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string,
99
+ unsupported: (node: any, message: string) => void): TransformCandidate[] {
107
100
  const candidates: TransformCandidate[] = [];
108
101
  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
- });
102
+ const resolve = createLexicalResolver(ts, sourceFile);
116
103
 
117
104
  visit(ts, sourceFile, (node: any) => {
118
105
  if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
@@ -124,38 +111,47 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
124
111
  }
125
112
  const delegateName = delegate.text;
126
113
  const level = node.expression.name.text;
114
+ const isConsole = delegateName === 'console' && !resolve(delegate, delegateName) && CONSOLE_LEVELS.includes(level);
115
+ const isHiLog = hiLogBindings.has(delegateName) &&
116
+ resolve(delegate, delegateName) === hiLogBindings.get(delegateName) && HILOG_LEVELS.includes(level);
117
+ if (!isConsole && !isHiLog) return;
118
+ // Preserve short-circuit semantics instead of transforming optional calls.
119
+ if (ts.isOptionalChain && ts.isOptionalChain(node)) {
120
+ unsupported(node, 'Optional Console/HiLog calls are left unchanged and are not automatically collected.');
121
+ return;
122
+ }
127
123
  const start = node.getStart(sourceFile);
128
124
  const end = node.end;
129
125
  const calleeEnd = node.expression.end;
130
126
  if (start < 0 || end > code.length || calleeEnd < start || calleeEnd > end) {
127
+ unsupported(node, 'The compiler returned an invalid source range for this logging call.');
131
128
  return;
132
129
  }
133
130
 
134
131
  const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
135
132
  start: argument.getStart(sourceFile),
136
- end: argument.end
133
+ end: argument.end,
134
+ spread: ts.isSpreadElement(argument)
137
135
  }));
138
- if (delegateName === 'console' && !consoleIsShadowed && CONSOLE_LEVELS.includes(level)) {
139
- candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments });
136
+ const statementSeparator = needsStatementSeparator(ts, node);
137
+ if (isConsole) {
138
+ candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'console', callArguments,
139
+ needsStatementSeparator: statementSeparator });
140
140
  return;
141
141
  }
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 });
142
+ if (callArguments.length >= 3 || callArguments.some(argument => argument.spread)) {
143
+ candidates.push({ start, end, calleeEnd, delegate: delegateName, level, source: 'hilog', callArguments,
144
+ needsStatementSeparator: statementSeparator });
145
+ } else {
146
+ unsupported(node, 'HiLog collection requires domain, tag and format arguments, or a spread argument supplying them.');
145
147
  }
146
148
  });
147
149
  return candidates;
148
150
  }
149
151
 
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;
152
+ function allocateCandidateNames(candidate: TransformCandidate, identifiers: Set<string>): string[] {
153
+ const prefix = candidate.source === 'console' ? '__ftConsole' : '__ftHiLog';
154
+ return ['Call', 'Args', 'Values'].map(suffix => allocateName(identifiers, prefix + suffix));
159
155
  }
160
156
 
161
157
  function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
@@ -187,42 +183,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
187
183
  candidate: TransformCandidate): string {
188
184
  const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
189
185
  renderCandidates(code, candidates, bridgeName, argument.start, argument.end));
186
+ const statementSeparator = candidate.needsStatementSeparator ? ';' : '';
187
+ const [original, args, values] = candidate.generatedNames!;
188
+ const valueType = '(Object | null | undefined)';
189
+ const originalType = candidate.source === 'console' ? `(...${args}: ${valueType}[]) => void` :
190
+ `(domain: number, tag: string, format: string, ...${values}: ${valueType}[]) => void`;
191
+ let body: string;
190
192
  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(', ')})`;
193
+ body = `${bridgeName}.console('${candidate.level}', (): void => ${original}(...${args}), ` +
194
+ `...(${args} as Object[]))`;
195
+ } else {
196
+ const header = `${args}[0] as number, ${args}[1] as string, ${args}[2] as string`;
197
+ body = `{ const ${values}: ${valueType}[] = ${args}.slice(3); ` +
198
+ `${bridgeName}.hilog('${candidate.level}', ${header}, (): void => ${original}(${header}, ...${values}), ` +
199
+ `...(${values} as Object[])); }`;
198
200
  }
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(', ')})`;
201
+ // Resolve and bind the original method before evaluating its arguments.
202
+ // Casting only the capture array keeps compatibility with older runtime HARs;
203
+ // null/undefined values themselves are forwarded unchanged.
204
+ // HiLog is a platform namespace, not an ArkTS object receiver. Its exported
205
+ // functions are static; console methods retain their receiver through bind.
206
+ const callee = `${candidate.delegate}.${candidate.level}`;
207
+ const captured = candidate.source === 'console' ? `${callee}.bind(${candidate.delegate})` : callee;
208
+ const argumentsText = renderedArguments.length > 0 ? ', ' + renderedArguments.join(', ') : '';
209
+ return `${statementSeparator}((${original}: ${originalType}, ...${args}: ${valueType}[]): void => ${body})` +
210
+ `(${captured} as ${originalType}${argumentsText})`;
226
211
  }
227
212
 
228
213
  function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
@@ -235,17 +220,26 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
235
220
  return offset;
236
221
  }
237
222
 
238
- export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string): string {
239
- if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER)) {
223
+ export function transformConsoleLogSource(ts: TypeScriptApi, id: string, code: string, readSource?: SourceReader,
224
+ reportDiagnostic?: SourceDiagnosticReporter): string {
225
+ if (!isApplicationArkTsSource(id) || code.includes(TRANSFORM_MARKER) ||
226
+ !hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
240
227
  return code;
241
228
  }
242
229
  const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
243
- const candidates = collectCandidates(ts, sourceFile, code);
230
+ const unsupported: Array<{ node: any; message: string }> = [];
231
+ const candidates = collectCandidates(ts, sourceFile, code, (node, message) => unsupported.push({ node, message }));
232
+ if (candidates.length === 0 && unsupported.length === 0) return code;
233
+ if (reportSourceParseError(ts, parseApplicationSource(ts, id, code), reportDiagnostic)) return code;
234
+ unsupported.forEach(item => reportSourceDiagnostic(sourceFile, item.node.getStart(sourceFile),
235
+ 'unsupported-log-call', item.message, reportDiagnostic));
244
236
  if (candidates.length === 0) {
245
237
  return code;
246
238
  }
247
239
 
248
- const bridgeName = selectBridgeName(collectIdentifierNames(ts, sourceFile));
240
+ const identifiers = collectIdentifierNames(ts, sourceFile);
241
+ const bridgeName = allocateName(identifiers, '__ftConsoleLogBridge');
242
+ candidates.forEach(candidate => { candidate.generatedNames = allocateCandidateNames(candidate, identifiers); });
249
243
  const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
250
244
  const insertionOffset = findImportInsertionOffset(ts, sourceFile);
251
245
  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
+ }