@platforma-sdk/tengo-builder 2.3.13 → 2.4.0

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.
@@ -20,33 +20,37 @@ const functionCallRE = (moduleName: string, fnName: string) => {
20
20
  `\\b${moduleName}\\.(?<fnCall>(?<fnName>`
21
21
  + fnName
22
22
  + `)\\s*\\(\\s*"(?<templateName>[^"]+)"\\s*\\))`,
23
+ 'g',
23
24
  );
24
25
  };
25
26
 
26
- export const newGetTemplateIdRE = (moduleName: string) => {
27
- return functionCallRE(moduleName, 'getTemplateId');
28
- };
29
- export const newGetSoftwareInfoRE = (moduleName: string) => {
30
- return functionCallRE(moduleName, 'getSoftwareInfo');
27
+ const functionCallLikeRE = (moduleName: string, fnName: string) => {
28
+ return new RegExp(
29
+ `\\b${moduleName}\\.(?<fnName>`
30
+ + fnName
31
+ + `)\\s*\\(`,
32
+ 'g',
33
+ );
31
34
  };
32
35
 
33
- const newImportTemplateRE = (moduleName: string) => {
34
- return functionCallRE(moduleName, 'importTemplate');
35
- };
36
- const newImportSoftwareRE = (moduleName: string) => {
37
- return functionCallRE(moduleName, 'importSoftware');
38
- };
39
- const newImportAssetRE = (moduleName: string) => {
40
- return functionCallRE(moduleName, 'importAsset');
41
- };
36
+ export const newGetTemplateIdRE = (moduleName: string) => functionCallRE(moduleName, 'getTemplateId');
37
+ export const newGetSoftwareInfoRE = (moduleName: string) => functionCallRE(moduleName, 'getSoftwareInfo');
38
+
39
+ const newImportTemplateRE = (moduleName: string) => functionCallRE(moduleName, 'importTemplate');
40
+ const newImportTemplateDetector = (moduleName: string) => functionCallLikeRE(moduleName, 'importTemplate');
41
+ const newImportSoftwareRE = (moduleName: string) => functionCallRE(moduleName, 'importSoftware');
42
+ const newImportSoftwareDetector = (moduleName: string) => functionCallLikeRE(moduleName, 'importSoftware');
43
+ const newImportAssetRE = (moduleName: string) => functionCallRE(moduleName, 'importAsset');
44
+ const newImportAssetDetector = (moduleName: string) => functionCallLikeRE(moduleName, 'importAsset');
42
45
 
43
46
  const emptyLineRE = /^\s*$/;
44
47
  const compilerOptionRE = /^\/\/tengo:[\w]/;
45
48
  const wrongCompilerOptionRE = /^\s*\/\/\s*tengo:\s*./;
46
- const inlineCommentRE = /\/\*.*?\*\//g; // .*? = non-greedy search
47
49
  const singlelineCommentRE = /^\s*(\/\/)/;
50
+ const singlelineTerminatedCommentRE = /^\s*\/\*.*\*\/\s*$/; // matches '^/* ... */$' comment lines as a special case of singleline comments.
48
51
  const multilineCommentStartRE = /^\s*\/\*/;
49
52
  const multilineCommentEndRE = /\*\//;
53
+ const multilineStatementRE = /[.,]\s*$/; // it is hard to consistently treat (\n"a"\n) multiline statements, we forbid them for now.
50
54
 
51
55
  // import could only be an assignment in a statement,
52
56
  // other ways could break a compilation.
@@ -163,7 +167,9 @@ function parseSourceData(
163
167
  let parserContext: sourceParserContext = {
164
168
  isInCommentBlock: false,
165
169
  canDetectOptions: true,
166
- tplDepREs: new Map<string, [ArtifactType, RegExp][]>(),
170
+ artifactImportREs: new Map<string, [ArtifactType, RegExp][]>(),
171
+ importLikeREs: new Map<string, [ArtifactType, RegExp][]>(),
172
+ multilineStatement: '',
167
173
  lineNo: 0,
168
174
  };
169
175
 
@@ -171,7 +177,7 @@ function parseSourceData(
171
177
  parserContext.lineNo++;
172
178
 
173
179
  try {
174
- const { line: processedLine, context: newContext, artifact, option } = parseSingleSourceLine(
180
+ const { line: processedLine, context: newContext, artifacts, option } = parseSingleSourceLine(
175
181
  logger,
176
182
  line,
177
183
  parserContext,
@@ -181,7 +187,7 @@ function parseSourceData(
181
187
  processedLines.push(processedLine);
182
188
  parserContext = newContext;
183
189
 
184
- if (artifact) {
190
+ for (const artifact of artifacts ?? []) {
185
191
  dependencySet.add(artifact);
186
192
  }
187
193
  if (option) {
@@ -200,12 +206,21 @@ function parseSourceData(
200
206
  };
201
207
  }
202
208
 
203
- interface sourceParserContext {
209
+ export type sourceParserContext = {
204
210
  isInCommentBlock: boolean;
205
211
  canDetectOptions: boolean;
206
- tplDepREs: Map<string, [ArtifactType, RegExp][]>;
212
+ artifactImportREs: Map<string, [ArtifactType, RegExp][]>;
213
+ importLikeREs: Map<string, [ArtifactType, RegExp][]>;
214
+ multilineStatement: string;
207
215
  lineNo: number;
208
- }
216
+ };
217
+
218
+ export type lineProcessingResult = {
219
+ line: string;
220
+ context: sourceParserContext;
221
+ artifacts: TypedArtifactName[];
222
+ option: CompilerOption | undefined;
223
+ };
209
224
 
210
225
  export function parseSingleSourceLine(
211
226
  logger: MiLogger,
@@ -213,20 +228,12 @@ export function parseSingleSourceLine(
213
228
  context: sourceParserContext,
214
229
  localPackageName: string,
215
230
  globalizeImports?: boolean,
216
- ): {
217
- line: string;
218
- context: sourceParserContext;
219
- artifact: TypedArtifactName | undefined;
220
- option: CompilerOption | undefined;
221
- } {
222
- // preprocess line and remove inline comments
223
- line = line.replaceAll(inlineCommentRE, '');
224
-
231
+ ): lineProcessingResult {
225
232
  if (context.isInCommentBlock) {
226
233
  if (multilineCommentEndRE.exec(line)) {
227
234
  context.isInCommentBlock = false;
228
235
  }
229
- return { line: '', context, artifact: undefined, option: undefined };
236
+ return { line: '', context, artifacts: [], option: undefined };
230
237
  }
231
238
 
232
239
  if (compilerOptionRE.exec(line)) {
@@ -236,127 +243,215 @@ export function parseSingleSourceLine(
236
243
  );
237
244
  throw new Error('tengo compiler options (\'//tengo:\' comments) can be set only in file header');
238
245
  }
239
- return { line, context, artifact: undefined, option: parseComplierOption(line) };
246
+ return { line, context, artifacts: [], option: parseComplierOption(line) };
240
247
  }
241
248
 
242
249
  if (wrongCompilerOptionRE.exec(line) && context.canDetectOptions) {
243
250
  logger.warn(
244
251
  `[line ${context.lineNo}]: text simillar to compiler option ('//tengo:...') was detected, but it has wrong format. Leave it as is, if you did not mean to use a line as compiler option. Or format it to '//tengo:<option>' otherwise (no spaces between '//' and 'tengo', no spaces between ':' and option name)`,
245
252
  );
246
- return { line, context, artifact: undefined, option: undefined };
253
+ return { line, context, artifacts: [], option: undefined };
247
254
  }
248
255
 
249
- if (singlelineCommentRE.test(line)) {
250
- return { line: '', context, artifact: undefined, option: undefined };
256
+ if (singlelineCommentRE.test(line) || singlelineTerminatedCommentRE.test(line)) {
257
+ return { line: '', context, artifacts: [], option: undefined };
251
258
  }
252
259
 
253
- if (multilineCommentStartRE.exec(line)) {
260
+ const canBeInlinedComment = line.includes('*/');
261
+ if (multilineCommentStartRE.exec(line) && !canBeInlinedComment) {
254
262
  context.isInCommentBlock = true;
255
- return { line: '', context, artifact: undefined, option: undefined };
263
+ return { line: '', context, artifacts: [], option: undefined };
256
264
  }
257
265
 
258
- if (line.includes('/*')) {
259
- throw new Error('malformed multiline comment');
266
+ const statement = context.multilineStatement + line.trim();
267
+
268
+ const mayContainAComment = line.includes('//') || line.includes('/*');
269
+ if (multilineStatementRE.test(line) && !mayContainAComment) {
270
+ // We accumulate multiline statements into single line before analyzing them.
271
+ // This dramatically simplifies parsing logic: things like
272
+ //
273
+ // assets.
274
+ // importSoftware("a:b");
275
+ //
276
+ // become simple 'assets.importSoftware("a:b");' for parser checks.
277
+ //
278
+ // For safety reasons, we never consider anything that 'may look like a comment'
279
+ // as a part of multiline statement to prevent joining things like
280
+ //
281
+ // someFnCall() // looks like multiline statement because of dot in the end of a comment.
282
+ //
283
+ // This problem also appears in multiline string literals, but I hope this will not
284
+ // cause problems in real life.
285
+
286
+ // We still try to process each line to globalize imports in case of complex constructions, when
287
+ // statements are stacked one into another:
288
+ // a.
289
+ // use(assets.importSoftware(":soft1")).
290
+ // use(assets.importSoftware(":soft2")).
291
+ // run()
292
+ // It is multiline, and it still requires import globalization mid-way, not just for the last line of statement
293
+ const result = processAssetImport(line, statement, context, localPackageName, globalizeImports);
294
+ context.multilineStatement += result.line.trim(); // accumulate the line after imports globalization.
295
+ return result;
260
296
  }
261
297
 
262
- if (emptyLineRE.exec(line)) {
263
- return { line, context, artifact: undefined, option: undefined };
298
+ context.multilineStatement = ''; // reset accumulated multiline statement parts once we reach statement end.
299
+
300
+ if (emptyLineRE.exec(statement)) {
301
+ return { line, context, artifacts: [], option: undefined };
264
302
  }
265
303
 
266
304
  // options could be only at the top of the file.
267
305
  context.canDetectOptions = false;
268
306
 
269
- const importInstruction = importRE.exec(line);
270
-
271
- if (importInstruction) {
272
- const iInfo = parseImport(line);
273
-
274
- // If we have plapi, ll or assets, then try to parse
275
- // getTemplateId, getSoftwareInfo, getSoftware and getAsset calls.
307
+ return processAssetImport(line, statement, context, localPackageName, globalizeImports);
308
+ }
276
309
 
277
- if (iInfo.module === 'plapi') {
278
- if (!context.tplDepREs.has(iInfo.module)) {
279
- context.tplDepREs.set(iInfo.module, [
280
- ['template', newGetTemplateIdRE(iInfo.alias)],
281
- ['software', newGetSoftwareInfoRE(iInfo.alias)],
282
- ]);
283
- }
284
- return { line, context, artifact: undefined, option: undefined };
310
+ function processModuleImport(
311
+ importInstruction: RegExpExecArray,
312
+ originalLine: string,
313
+ statement: string,
314
+ context: sourceParserContext,
315
+ localPackageName: string,
316
+ globalizeImports?: boolean,
317
+ ): lineProcessingResult {
318
+ const iInfo = parseImport(statement);
319
+
320
+ // If we have plapi, ll or assets, then try to parse
321
+ // getTemplateId, getSoftwareInfo, getSoftware and getAsset calls.
322
+
323
+ if (iInfo.module === 'plapi') {
324
+ if (!context.artifactImportREs.has(iInfo.module)) {
325
+ context.artifactImportREs.set(iInfo.module, [
326
+ ['template', newGetTemplateIdRE(iInfo.alias)],
327
+ ['software', newGetSoftwareInfoRE(iInfo.alias)],
328
+ ]);
285
329
  }
330
+ return { line: originalLine, context, artifacts: [], option: undefined };
331
+ }
286
332
 
287
- if (
288
- iInfo.module === '@milaboratory/tengo-sdk:ll'
289
- || iInfo.module === '@platforma-sdk/workflow-tengo:ll'
290
- || ((localPackageName === '@milaboratory/tengo-sdk'
291
- || localPackageName === '@platforma-sdk/workflow-tengo')
292
- && iInfo.module === ':ll')
293
- ) {
294
- if (!context.tplDepREs.has(iInfo.module)) {
295
- context.tplDepREs.set(iInfo.module, [
296
- ['template', newImportTemplateRE(iInfo.alias)],
297
- ['software', newImportSoftwareRE(iInfo.alias)],
298
- ]);
299
- }
333
+ if (
334
+ iInfo.module === '@milaboratory/tengo-sdk:ll'
335
+ || iInfo.module === '@platforma-sdk/workflow-tengo:ll'
336
+ || ((localPackageName === '@milaboratory/tengo-sdk'
337
+ || localPackageName === '@platforma-sdk/workflow-tengo')
338
+ && iInfo.module === ':ll')
339
+ ) {
340
+ if (!context.artifactImportREs.has(iInfo.module)) {
341
+ context.artifactImportREs.set(iInfo.module, [
342
+ ['template', newImportTemplateRE(iInfo.alias)],
343
+ ['software', newImportSoftwareRE(iInfo.alias)],
344
+ ]);
300
345
  }
346
+ }
301
347
 
302
- if (
303
- iInfo.module === '@milaboratory/tengo-sdk:assets'
304
- || iInfo.module === '@platforma-sdk/workflow-tengo:assets'
305
- || ((localPackageName === '@milaboratory/tengo-sdk'
306
- || localPackageName === '@platforma-sdk/workflow-tengo')
307
- && iInfo.module === ':assets')
308
- ) {
309
- if (!context.tplDepREs.has(iInfo.module)) {
310
- context.tplDepREs.set(iInfo.module, [
311
- ['template', newImportTemplateRE(iInfo.alias)],
312
- ['software', newImportSoftwareRE(iInfo.alias)],
313
- ['asset', newImportAssetRE(iInfo.alias)],
314
- ]);
315
- }
348
+ if (
349
+ iInfo.module === '@milaboratory/tengo-sdk:assets'
350
+ || iInfo.module === '@platforma-sdk/workflow-tengo:assets'
351
+ || ((localPackageName === '@milaboratory/tengo-sdk'
352
+ || localPackageName === '@platforma-sdk/workflow-tengo')
353
+ && iInfo.module === ':assets')
354
+ ) {
355
+ if (!context.artifactImportREs.has(iInfo.module)) {
356
+ context.artifactImportREs.set(iInfo.module, [
357
+ ['template', newImportTemplateRE(iInfo.alias)],
358
+ ['software', newImportSoftwareRE(iInfo.alias)],
359
+ ['asset', newImportAssetRE(iInfo.alias)],
360
+ ]);
361
+ context.importLikeREs.set(iInfo.module, [
362
+ ['template', newImportTemplateDetector(iInfo.alias)],
363
+ ['software', newImportSoftwareDetector(iInfo.alias)],
364
+ ['asset', newImportAssetDetector(iInfo.alias)],
365
+ ]);
316
366
  }
367
+ }
317
368
 
318
- const artifact = parseArtifactName(iInfo.module, 'library', localPackageName);
319
- if (!artifact) {
320
- // not a Platforma Tengo library import
321
- return { line, context, artifact: undefined, option: undefined };
322
- }
369
+ const artifact = parseArtifactName(iInfo.module, 'library', localPackageName);
370
+ if (!artifact) {
371
+ // not a Platforma Tengo library import
372
+ return { line: originalLine, context, artifacts: [], option: undefined };
373
+ }
323
374
 
324
- if (globalizeImports) {
325
- line = line.replace(importInstruction[0], ` := import("${artifact.pkg}:${artifact.id}")`);
326
- }
375
+ if (globalizeImports) {
376
+ originalLine = originalLine.replace(importInstruction[0], ` := import("${artifact.pkg}:${artifact.id}")`);
377
+ }
378
+
379
+ return { line: originalLine, context, artifacts: [artifact], option: undefined };
380
+ }
327
381
 
328
- return { line, context, artifact, option: undefined };
382
+ function processAssetImport(
383
+ originalLine: string,
384
+ statement: string,
385
+ context: sourceParserContext,
386
+ localPackageName: string,
387
+ globalizeImports?: boolean,
388
+ ): lineProcessingResult {
389
+ if (emptyLineRE.exec(statement)) {
390
+ return { line: originalLine, context, artifacts: [], option: undefined };
329
391
  }
330
392
 
331
- if (context.tplDepREs.size > 0) {
332
- for (const [_, artifactRE] of context.tplDepREs) {
393
+ // options could be only at the top of the file.
394
+ context.canDetectOptions = false;
395
+
396
+ const importInstruction = importRE.exec(statement);
397
+
398
+ if (importInstruction) {
399
+ return processModuleImport(importInstruction, originalLine, statement, context, localPackageName, globalizeImports);
400
+ }
401
+
402
+ if (context.artifactImportREs.size > 0) {
403
+ for (const [_, artifactRE] of context.artifactImportREs) {
333
404
  for (const [artifactType, re] of artifactRE) {
334
- const match = re.exec(line);
335
- if (!match || !match.groups) {
405
+ // Find all matches in the statement
406
+ const matches = Array.from(statement.matchAll(re));
407
+ if (matches.length === 0) {
336
408
  continue;
337
409
  }
338
410
 
339
- const { fnCall, templateName, fnName } = match.groups;
340
-
341
- if (!fnCall || !templateName || !fnName) {
342
- throw Error(`failed to parse template import statement`);
411
+ const artifacts: TypedArtifactName[] = [];
412
+ for (let i = matches.length - 1; i >= 0; i--) {
413
+ const match = matches[i];
414
+ if (!match || !match.groups) {
415
+ continue;
416
+ }
417
+
418
+ const { fnCall, templateName, fnName } = match.groups;
419
+
420
+ if (!fnCall || !templateName || !fnName) {
421
+ throw Error(`failed to parse template import statement`);
422
+ }
423
+
424
+ const artifact = parseArtifactName(templateName, artifactType, localPackageName);
425
+ if (!artifact) {
426
+ throw Error(`failed to parse artifact name in ${fnName} import statement`);
427
+ }
428
+ artifacts.push(artifact);
429
+
430
+ if (globalizeImports) {
431
+ // Replace all occurrences of this fnCall in originalLine
432
+ originalLine = originalLine.replaceAll(fnCall, `${fnName}("${artifact.pkg}:${artifact.id}")`);
433
+ }
343
434
  }
344
435
 
345
- const artifact = parseArtifactName(templateName, artifactType, localPackageName);
346
- if (!artifact) {
347
- throw Error(`failed to parse artifact name in ${fnName} import statement`);
348
- }
436
+ return { line: originalLine, context, artifacts, option: undefined };
437
+ }
438
+ }
439
+ }
349
440
 
350
- if (globalizeImports) {
351
- line = line.replace(fnCall, `${fnName}("${artifact.pkg}:${artifact.id}")`);
441
+ if (context.importLikeREs.size > 0) {
442
+ for (const [_, artifactRE] of context.importLikeREs) {
443
+ for (const [artifactType, re] of artifactRE) {
444
+ const match = re.exec(statement);
445
+ if (!match || !match.groups) {
446
+ continue;
352
447
  }
353
448
 
354
- return { line, context, artifact, option: undefined };
449
+ throw Error(`incorrect '${artifactType}' import statement: use string literal as ID (variables are not allowed) in the same line with brackets (i.e. 'importSoftware("sw:main")').`);
355
450
  }
356
451
  }
357
452
  }
358
453
 
359
- return { line, context, artifact: undefined, option: undefined };
454
+ return { line: originalLine, context, artifacts: [], option: undefined };
360
455
  }
361
456
 
362
457
  interface ImportInfo {
@@ -43,8 +43,10 @@ plapiCustomName := import("plapi" )
43
43
 
44
44
  notplapiCustomName.getTemplateId( "some other:parameter")
45
45
 
46
- plapiCustomName.getTemplateIdAnother("sss:kkk" )
47
- plapiCustomName.getSoftwareInfo(":software-1")
46
+ /* inline comment */ plapiCustomName.getTemplateIdAnother("sss:kkk" )
47
+ plapiCustomName.getSoftwareInfo(":software-1") /* inline comment */
48
+
49
+ someCall("comment-like string literal/*")
48
50
 
49
51
  export {
50
52
  "some": "value",
@@ -58,8 +60,10 @@ plapiCustomName := import("plapi" )
58
60
 
59
61
  notplapiCustomName.getTemplateId( "some other:parameter")
60
62
 
61
- plapiCustomName.getTemplateIdAnother("sss:kkk" )
62
- plapiCustomName.getSoftwareInfo("current-package:software-1")
63
+ /* inline comment */ plapiCustomName.getTemplateIdAnother("sss:kkk" )
64
+ plapiCustomName.getSoftwareInfo("current-package:software-1") /* inline comment */
65
+
66
+ someCall("comment-like string literal/*")
63
67
 
64
68
  export {
65
69
  "some": "value",
@@ -82,6 +86,24 @@ ll := import("@platforma-sdk/workflow-tengo:assets")
82
86
  a := import(":non-existent-library")
83
87
  */
84
88
 
89
+ tplID := ll.importTemplate("package2:template-1")
90
+ softwareID := ll.importSoftware("package2:software-1")
91
+ assetID := ll.importAsset("package2:asset-1")
92
+
93
+ export {
94
+ "some": "value",
95
+ "template1": ll.importTemplate("current-package:local-template-2"),
96
+ "template2": tplID,
97
+ }
98
+ `;
99
+ export const testLocalLib2SrcNormalized = `
100
+ otherLib := import("package1:someid")
101
+ ll := import("@platforma-sdk/workflow-tengo:assets")
102
+
103
+
104
+
105
+
106
+
85
107
  tplID := ll.importTemplate("package2:template-1")
86
108
  softwareID := ll.importSoftware("package2:software-1")
87
109
  assetID := ll.importAsset("package2:asset-1")
@@ -269,3 +291,27 @@ export const testPackage2BrokenHash: TestArtifactSource[] = [
269
291
  src: testLocalTpl3SrcWrongOverride,
270
292
  },
271
293
  ];
294
+
295
+ export const testTrickyCasesSrc = `
296
+ assets := import("@platforma-sdk/workflow-tengo:assets")
297
+ exec := import("@platforma-sdk/workflow-tengo:exec")
298
+
299
+ run := exec.builder().
300
+ processWorkdir("p", assets.importTemplate(":test.wd_processor_1"), {}).
301
+ processWorkdir("q", assets.
302
+ importTemplate(":test.wd_processor_2"),
303
+ {}).
304
+ run()
305
+ `;
306
+
307
+ export const testTrickyCasesNormalized = `
308
+ assets := import("@platforma-sdk/workflow-tengo:assets")
309
+ exec := import("@platforma-sdk/workflow-tengo:exec")
310
+
311
+ run := exec.builder().
312
+ processWorkdir("p", assets.importTemplate("stub-pkg:test.wd_processor_1"), {}).
313
+ processWorkdir("q", assets.
314
+ importTemplate("stub-pkg:test.wd_processor_2"),
315
+ {}).
316
+ run()
317
+ `;