@cloudcare/hvigor-ohos-plugin 0.1.0 → 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,28 +7,12 @@
7
7
  * never changed.
8
8
  */
9
9
 
10
- declare const require: {
11
- main: {
12
- require(moduleName: string): any;
13
- };
14
- };
10
+ import { hasRuntimeDependency, SourceReader } from './ft-transform-context';
11
+ import { createLexicalResolver } from './ft-lexical-scope';
12
+ import { allocateName, collectIdentifierNames } from './ft-generated-names';
15
13
 
16
14
  type TypeScriptApi = any;
17
15
 
18
- interface CompilePluginContext {
19
- share: {
20
- projectConfig: {
21
- etsLoaderPath: string;
22
- };
23
- };
24
- }
25
-
26
- interface SharedSourceTransformRegistry {
27
- originalReadFile: (fileName: string, options?: any) => any;
28
- buffer: any;
29
- transforms: Map<string, (fileName: string, code: string) => string>;
30
- }
31
-
32
16
  interface TransformCandidate {
33
17
  start: number;
34
18
  end: number;
@@ -37,11 +21,14 @@ interface TransformCandidate {
37
21
  level: string;
38
22
  source: 'console' | 'hilog';
39
23
  callArguments: SourceRange[];
24
+ needsStatementSeparator: boolean;
25
+ generatedNames?: string[];
40
26
  }
41
27
 
42
28
  interface SourceRange {
43
29
  start: number;
44
30
  end: number;
31
+ spread: boolean;
45
32
  }
46
33
 
47
34
  const HILOG_MODULES: string[] = ['@ohos.hilog', '@kit.PerformanceAnalysisKit'];
@@ -52,10 +39,6 @@ const BRIDGE_IMPORT = '@guancecloud/ft_sdk/Index';
52
39
  // incremental/daemon build. Keep a marker in the in-memory result so a later
53
40
  // reader does not wrap the original console/HiLog call a second time.
54
41
  const TRANSFORM_MARKER = '/* guance-console-log-transform:instrumented */';
55
- const SOURCE_TRANSFORM_REGISTRY_KEY = '__guanceHvigorSourceTransformRegistry';
56
- const TRANSFORM_ID = 'guance-console-log-transform';
57
-
58
- let registeredFileSystem: any = null;
59
42
 
60
43
  export function isApplicationArkTsSource(id: string): boolean {
61
44
  const normalizedId = id.replace(/\\/g, '/');
@@ -69,77 +52,58 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
69
52
  ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
70
53
  }
71
54
 
72
- function collectIdentifierNames(ts: TypeScriptApi, sourceFile: any): Set<string> {
73
- const identifiers = new Set<string>();
74
- visit(ts, sourceFile, (node: any) => {
75
- if (ts.isIdentifier(node)) {
76
- identifiers.add(node.text);
77
- }
78
- });
79
- return identifiers;
80
- }
81
-
82
- function isNameDeclaredOutsideImports(ts: TypeScriptApi, sourceFile: any, name: string): boolean {
83
- let declared = false;
84
- visit(ts, sourceFile, (node: any) => {
85
- if (declared || ts.isImportDeclaration(node)) {
86
- return;
87
- }
88
- if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) ||
89
- ts.isParameter(node) || ts.isEnumDeclaration(node) || ts.isInterfaceDeclaration(node) ||
90
- ts.isTypeAliasDeclaration(node)) {
91
- const declarationName = node.name;
92
- if (declarationName && ts.isIdentifier(declarationName) && declarationName.text === name) {
93
- declared = true;
94
- }
95
- }
96
- });
97
- return declared;
98
- }
99
-
100
- function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Set<string> {
101
- const bindings = new Set<string>();
55
+ function collectHiLogBindings(ts: TypeScriptApi, sourceFile: any): Map<string, any> {
56
+ const bindings = new Map<string, any>();
102
57
  sourceFile.statements.forEach((statement: any) => {
103
58
  if (!ts.isImportDeclaration(statement) || !statement.moduleSpecifier ||
104
59
  !ts.isStringLiteral(statement.moduleSpecifier) ||
105
- !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause) {
60
+ !HILOG_MODULES.includes(statement.moduleSpecifier.text) || !statement.importClause || statement.importClause.isTypeOnly) {
106
61
  return;
107
62
  }
108
63
 
109
64
  const importClause = statement.importClause;
110
65
  if (importClause.name) {
111
- bindings.add(importClause.name.text);
66
+ bindings.set(importClause.name.text, importClause);
112
67
  }
113
68
  if (!importClause.namedBindings) {
114
69
  return;
115
70
  }
116
71
  if (ts.isNamespaceImport(importClause.namedBindings)) {
117
- bindings.add(importClause.namedBindings.name.text);
72
+ bindings.set(importClause.namedBindings.name.text, importClause.namedBindings);
118
73
  return;
119
74
  }
120
75
  importClause.namedBindings.elements.forEach((element: any) => {
121
76
  const importedName = element.propertyName ? element.propertyName.text : element.name.text;
122
- if (importedName === 'hilog') {
123
- bindings.add(element.name.text);
77
+ if (importedName === 'hilog' && !element.isTypeOnly) {
78
+ bindings.set(element.name.text, element);
124
79
  }
125
80
  });
126
81
  });
127
82
  return bindings;
128
83
  }
129
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
+
130
97
  function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): TransformCandidate[] {
131
98
  const candidates: TransformCandidate[] = [];
132
99
  const hiLogBindings = collectHiLogBindings(ts, sourceFile);
133
- const consoleIsShadowed = isNameDeclaredOutsideImports(ts, sourceFile, 'console');
134
- const shadowedHiLogBindings = new Set<string>();
135
- hiLogBindings.forEach((binding: string) => {
136
- if (isNameDeclaredOutsideImports(ts, sourceFile, binding)) {
137
- shadowedHiLogBindings.add(binding);
138
- }
139
- });
100
+ const resolve = createLexicalResolver(ts, sourceFile);
140
101
 
141
102
  visit(ts, sourceFile, (node: any) => {
142
- 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))) {
143
107
  return;
144
108
  }
145
109
  const delegate = node.expression.expression;
@@ -157,29 +121,27 @@ function collectCandidates(ts: TypeScriptApi, sourceFile: any, code: string): Tr
157
121
 
158
122
  const callArguments: SourceRange[] = node.arguments.map((argument: any) => ({
159
123
  start: argument.getStart(sourceFile),
160
- end: argument.end
124
+ end: argument.end,
125
+ spread: ts.isSpreadElement(argument)
161
126
  }));
162
- if (delegateName === 'console' && !consoleIsShadowed && CONSOLE_LEVELS.includes(level)) {
163
- 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 });
164
131
  return;
165
132
  }
166
- if (hiLogBindings.has(delegateName) && !shadowedHiLogBindings.has(delegateName) &&
167
- HILOG_LEVELS.includes(level) && callArguments.length >= 3) {
168
- 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 });
169
137
  }
170
138
  });
171
139
  return candidates;
172
140
  }
173
141
 
174
- function selectBridgeName(identifiers: Set<string>): string {
175
- const baseName = '__ftConsoleLogBridge';
176
- let bridgeName = baseName;
177
- let suffix = 1;
178
- while (identifiers.has(bridgeName)) {
179
- bridgeName = `${baseName}${suffix}`;
180
- suffix++;
181
- }
182
- 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));
183
145
  }
184
146
 
185
147
  function isNestedIn(candidate: TransformCandidate, parent: TransformCandidate): boolean {
@@ -211,42 +173,31 @@ function renderCandidate(code: string, candidates: TransformCandidate[], bridgeN
211
173
  candidate: TransformCandidate): string {
212
174
  const renderedArguments = candidate.callArguments.map((argument: SourceRange) =>
213
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;
214
182
  if (candidate.source === 'console') {
215
- const argumentNames = renderedArguments.map((_argument: string, index: number) => `__ftConsoleArg${index}`);
216
- const parameterDeclarations = argumentNames.map((name: string) => `${name}: Object`).join(', ');
217
- const originalCallArguments = argumentNames.map((name: string) => `${name} as string`);
218
- const originalCall = `${candidate.delegate}.${candidate.level}(${originalCallArguments.join(', ')})`;
219
- const captureArguments = argumentNames.length > 0 ? `, ${argumentNames.join(', ')}` : '';
220
- return `((${parameterDeclarations}): void => ${bridgeName}.console('${candidate.level}', ` +
221
- `(): 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[])); }`;
222
190
  }
223
-
224
- const argumentNames = renderedArguments.map((_argument: string, index: number) => {
225
- if (index === 0) {
226
- return '__ftHiLogDomain';
227
- }
228
- if (index === 1) {
229
- return '__ftHiLogTag';
230
- }
231
- if (index === 2) {
232
- return '__ftHiLogFormat';
233
- }
234
- return `__ftHiLogArg${index - 3}`;
235
- });
236
- const parameterDeclarations = argumentNames.map((name: string, index: number) => {
237
- if (index === 0) {
238
- return `${name}: number`;
239
- }
240
- if (index === 1 || index === 2) {
241
- return `${name}: string`;
242
- }
243
- return `${name}: Object`;
244
- }).join(', ');
245
- const originalCall = `${candidate.delegate}.${candidate.level}(${argumentNames.join(', ')})`;
246
- const captureArguments = argumentNames.length > 3 ? `, ${argumentNames.slice(3).join(', ')}` : '';
247
- return `((${parameterDeclarations}): void => ${bridgeName}.hilog('${candidate.level}', ` +
248
- `${argumentNames[0]}, ${argumentNames[1]}, ${argumentNames[2]}, (): void => ${originalCall}` +
249
- `${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})`;
250
201
  }
251
202
 
252
203
  function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
@@ -259,8 +210,9 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
259
210
  return offset;
260
211
  }
261
212
 
262
- function rewriteSource(ts: TypeScriptApi, id: string, code: string): string {
263
- 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)) {
264
216
  return code;
265
217
  }
266
218
  const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
@@ -269,7 +221,9 @@ function rewriteSource(ts: TypeScriptApi, id: string, code: string): string {
269
221
  return code;
270
222
  }
271
223
 
272
- 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); });
273
227
  const transformedCode = renderCandidates(code, candidates, bridgeName, 0, code.length);
274
228
  const insertionOffset = findImportInsertionOffset(ts, sourceFile);
275
229
  const transformedInsertionOffset = renderCandidates(code, candidates, bridgeName, 0, insertionOffset).length;
@@ -279,61 +233,3 @@ function rewriteSource(ts: TypeScriptApi, id: string, code: string): string {
279
233
  return transformedCode.slice(0, transformedInsertionOffset) + bridgeImport +
280
234
  transformedCode.slice(transformedInsertionOffset);
281
235
  }
282
-
283
- function installSourceReader(context: CompilePluginContext): void {
284
- if (registeredFileSystem !== null) {
285
- return;
286
- }
287
-
288
- const typeScriptPath = `${context.share.projectConfig.etsLoaderPath}/node_modules/typescript`;
289
- const ts = require.main.require(typeScriptPath);
290
- const fileSystem = require.main.require('fs');
291
- let registry = fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
292
- if (!registry) {
293
- registry = {
294
- originalReadFile: fileSystem.readFileSync as (fileName: string, options?: any) => any,
295
- buffer: require.main.require('buffer'),
296
- transforms: new Map<string, (fileName: string, code: string) => string>()
297
- };
298
- fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] = registry;
299
- fileSystem.readFileSync = (fileName: string, options?: any): any => {
300
- const content = registry!.originalReadFile(fileName, options);
301
- if (typeof fileName !== 'string' || !isApplicationArkTsSource(fileName)) {
302
- return content;
303
- }
304
- let transformedCode = typeof content === 'string' ? content : content.toString();
305
- registry!.transforms.forEach((transform: (id: string, code: string) => string) => {
306
- transformedCode = transform(fileName, transformedCode);
307
- });
308
- return typeof content === 'string' ? transformedCode : registry!.buffer.Buffer.from(transformedCode);
309
- };
310
- }
311
- registry.transforms.set(TRANSFORM_ID, (fileName: string, code: string) => rewriteSource(ts, fileName, code));
312
- registeredFileSystem = fileSystem;
313
- }
314
-
315
- function restoreSourceReader(): void {
316
- if (registeredFileSystem === null) {
317
- return;
318
- }
319
- const registry = registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
320
- if (registry) {
321
- registry.transforms.delete(TRANSFORM_ID);
322
- if (registry.transforms.size === 0) {
323
- registeredFileSystem.readFileSync = registry.originalReadFile;
324
- delete registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY];
325
- }
326
- }
327
- registeredFileSystem = null;
328
- }
329
-
330
- export default {
331
- name: 'guance-console-log-transform',
332
- order: 'first',
333
- buildStart(this: CompilePluginContext): void {
334
- installSourceReader(this);
335
- },
336
- buildEnd(): void {
337
- restoreSourceReader();
338
- }
339
- };
@@ -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
+ }