@platforma-sdk/tengo-builder 4.1.1 → 4.1.3

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.
@@ -16,14 +16,26 @@ function escapeForRegExp(s) {
16
16
  /**
17
17
  * Find imports the source does not use.
18
18
  *
19
- * An import is used when its alias appears with a dot somewhere else in the
20
- * code: 'text.split(...)'. A module passed around as a value, say
21
- * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an
22
- * import the source no longer needs is nothing, while the cost of dropping one
23
- * it does need is a broken build.
19
+ * An import counts as used when its alias appears elsewhere in the code as a
20
+ * name of its own. Three shapes count:
24
21
  *
25
- * Throws when the source does not parse, with the same line context the
26
- * compiler reports.
22
+ * - dereferenced: 'text.split(x)'
23
+ * - called: 'doIt(x)', for a lib that exports a bare function
24
+ * - passed as a value: 'myFunc(text)'
25
+ *
26
+ * Two shapes do not count. A longer name that ends with the alias ('xtext') is
27
+ * a different name. A declaration that shadows the alias ('text := ...') binds
28
+ * the name. It does not read the import.
29
+ *
30
+ * Every other occurrence counts. This includes a member of another value
31
+ * ('opts.text') and text inside a string literal ('"a text file"'). The check
32
+ * is wide on purpose. An import that the source no longer needs costs nothing.
33
+ * A dropped import that the source needs breaks the build. The predicate must
34
+ * stay wider than every earlier predicate. A release then reports fewer unused
35
+ * imports, and never more.
36
+ *
37
+ * Throws when the source does not parse. The error carries the same line
38
+ * context that the compiler reports.
27
39
  */
28
40
  function findUnusedImports(logger, src, localPackageName, srcName) {
29
41
  const imports = [];
@@ -46,7 +58,7 @@ function findUnusedImports(logger, src, localPackageName, srcName) {
46
58
  }
47
59
  }
48
60
  return imports.filter((im) => {
49
- const usageRE = new RegExp(`\\b${escapeForRegExp(im.alias)}\\s*\\.`);
61
+ const usageRE = new RegExp(`(?<!\\w)${escapeForRegExp(im.alias)}\\b(?!\\s*:=)`);
50
62
  return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));
51
63
  });
52
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"imports.cjs","names":["parseSingleSourceLine"],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import is used when its alias appears with a dot somewhere else in the\n * code: 'text.split(...)'. A module passed around as a value, say\n * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an\n * import the source no longer needs is nothing, while the cost of dropping one\n * it does need is a broken build.\n *\n * Throws when the source does not parse, with the same line context the\n * compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`\\\\b${escapeForRegExp(im.alias)}\\\\s*\\\\.`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;AAcA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAASA,eAAAA,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,MAAM,gBAAgB,GAAG,KAAK,EAAE,QAAQ;EACnE,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
1
+ {"version":3,"file":"imports.cjs","names":["parseSingleSourceLine"],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import counts as used when its alias appears elsewhere in the code as a\n * name of its own. Three shapes count:\n *\n * - dereferenced: 'text.split(x)'\n * - called: 'doIt(x)', for a lib that exports a bare function\n * - passed as a value: 'myFunc(text)'\n *\n * Two shapes do not count. A longer name that ends with the alias ('xtext') is\n * a different name. A declaration that shadows the alias ('text := ...') binds\n * the name. It does not read the import.\n *\n * Every other occurrence counts. This includes a member of another value\n * ('opts.text') and text inside a string literal ('\"a text file\"'). The check\n * is wide on purpose. An import that the source no longer needs costs nothing.\n * A dropped import that the source needs breaks the build. The predicate must\n * stay wider than every earlier predicate. A release then reports fewer unused\n * imports, and never more.\n *\n * Throws when the source does not parse. The error carries the same line\n * context that the compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`(?<!\\\\w)${escapeForRegExp(im.alias)}\\\\b(?!\\\\s*:=)`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAASA,eAAAA,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,WAAW,gBAAgB,GAAG,KAAK,EAAE,cAAc;EAC9E,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
@@ -16,14 +16,26 @@ function escapeForRegExp(s) {
16
16
  /**
17
17
  * Find imports the source does not use.
18
18
  *
19
- * An import is used when its alias appears with a dot somewhere else in the
20
- * code: 'text.split(...)'. A module passed around as a value, say
21
- * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an
22
- * import the source no longer needs is nothing, while the cost of dropping one
23
- * it does need is a broken build.
19
+ * An import counts as used when its alias appears elsewhere in the code as a
20
+ * name of its own. Three shapes count:
24
21
  *
25
- * Throws when the source does not parse, with the same line context the
26
- * compiler reports.
22
+ * - dereferenced: 'text.split(x)'
23
+ * - called: 'doIt(x)', for a lib that exports a bare function
24
+ * - passed as a value: 'myFunc(text)'
25
+ *
26
+ * Two shapes do not count. A longer name that ends with the alias ('xtext') is
27
+ * a different name. A declaration that shadows the alias ('text := ...') binds
28
+ * the name. It does not read the import.
29
+ *
30
+ * Every other occurrence counts. This includes a member of another value
31
+ * ('opts.text') and text inside a string literal ('"a text file"'). The check
32
+ * is wide on purpose. An import that the source no longer needs costs nothing.
33
+ * A dropped import that the source needs breaks the build. The predicate must
34
+ * stay wider than every earlier predicate. A release then reports fewer unused
35
+ * imports, and never more.
36
+ *
37
+ * Throws when the source does not parse. The error carries the same line
38
+ * context that the compiler reports.
27
39
  */
28
40
  function findUnusedImports(logger, src, localPackageName, srcName) {
29
41
  const imports = [];
@@ -46,7 +58,7 @@ function findUnusedImports(logger, src, localPackageName, srcName) {
46
58
  }
47
59
  }
48
60
  return imports.filter((im) => {
49
- const usageRE = new RegExp(`\\b${escapeForRegExp(im.alias)}\\s*\\.`);
61
+ const usageRE = new RegExp(`(?<!\\w)${escapeForRegExp(im.alias)}\\b(?!\\s*:=)`);
50
62
  return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));
51
63
  });
52
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"imports.js","names":[],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import is used when its alias appears with a dot somewhere else in the\n * code: 'text.split(...)'. A module passed around as a value, say\n * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an\n * import the source no longer needs is nothing, while the cost of dropping one\n * it does need is a broken build.\n *\n * Throws when the source does not parse, with the same line context the\n * compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`\\\\b${escapeForRegExp(im.alias)}\\\\s*\\\\.`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;AAcA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAAS,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,MAAM,gBAAgB,GAAG,KAAK,EAAE,QAAQ;EACnE,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
1
+ {"version":3,"file":"imports.js","names":[],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import counts as used when its alias appears elsewhere in the code as a\n * name of its own. Three shapes count:\n *\n * - dereferenced: 'text.split(x)'\n * - called: 'doIt(x)', for a lib that exports a bare function\n * - passed as a value: 'myFunc(text)'\n *\n * Two shapes do not count. A longer name that ends with the alias ('xtext') is\n * a different name. A declaration that shadows the alias ('text := ...') binds\n * the name. It does not read the import.\n *\n * Every other occurrence counts. This includes a member of another value\n * ('opts.text') and text inside a string literal ('\"a text file\"'). The check\n * is wide on purpose. An import that the source no longer needs costs nothing.\n * A dropped import that the source needs breaks the build. The predicate must\n * stay wider than every earlier predicate. A release then reports fewer unused\n * imports, and never more.\n *\n * Throws when the source does not parse. The error carries the same line\n * context that the compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`(?<!\\\\w)${escapeForRegExp(im.alias)}\\\\b(?!\\\\s*:=)`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAAS,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,WAAW,gBAAgB,GAAG,KAAK,EAAE,cAAc;EAC9E,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/tengo-builder",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "Pl Tengo Template Builder",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -17,8 +17,8 @@
17
17
  "commander": "^15.0.0",
18
18
  "winston": "^3.17.0",
19
19
  "@milaboratories/resolve-helper": "1.1.3",
20
- "@milaboratories/ts-helpers": "1.8.6",
21
- "@milaboratories/pl-model-backend": "1.4.27"
20
+ "@milaboratories/pl-model-backend": "1.4.28",
21
+ "@milaboratories/ts-helpers": "1.8.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "~24.5.2",
@@ -26,8 +26,8 @@
26
26
  "typescript": "7.0.2",
27
27
  "vitest": "^4.1.3",
28
28
  "@milaboratories/build-configs": "2.0.2",
29
- "@milaboratories/ts-configs": "1.4.0",
30
- "@milaboratories/ts-builder": "1.7.2"
29
+ "@milaboratories/ts-builder": "1.7.2",
30
+ "@milaboratories/ts-configs": "1.4.0"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=22"
@@ -99,18 +99,92 @@ doSomething := func() {
99
99
  expect(unusedAliases(src)).toEqual([]);
100
100
  });
101
101
 
102
- test("a module the code only passes around counts as unused", () => {
103
- // We ask for the dot on purpose: keeping an import too long costs
104
- // nothing, dropping one the source needs breaks the build.
102
+ test("a lib called directly counts as used", () => {
103
+ const src = `calculateExportSpecs := import(":export-specs")
104
+
105
+ doSomething := func(x) {
106
+ return calculateExportSpecs(x)
107
+ }
108
+ `;
109
+ expect(unusedAliases(src)).toEqual([]);
110
+ });
111
+
112
+ test("a module passed as a value counts as used", () => {
105
113
  const src = `text := import("text")
106
114
 
107
115
  doSomething := func() {
108
116
  return apply(text)
109
117
  }
118
+ `;
119
+ expect(unusedAliases(src)).toEqual([]);
120
+ });
121
+
122
+ test("an alias used only as an array element or a map value counts as used", () => {
123
+ const src = `ll := import(":ll")
124
+ sets := import(":sets")
125
+
126
+ doSomething := func() {
127
+ return { handlers: [ll], fallback: sets }
128
+ }
129
+ `;
130
+ expect(unusedAliases(src)).toEqual([]);
131
+ });
132
+
133
+ test("an alias returned bare counts as used", () => {
134
+ const src = `maps := import(":maps")
135
+
136
+ doSomething := func() {
137
+ return maps
138
+ }
139
+ `;
140
+ expect(unusedAliases(src)).toEqual([]);
141
+ });
142
+
143
+ test("the import line itself is not a usage", () => {
144
+ const src = `calculateExportSpecs := import(":export-specs")
145
+
146
+ doSomething := func(x) {
147
+ return x
148
+ }
149
+ `;
150
+ expect(unusedAliases(src)).toEqual(["calculateExportSpecs"]);
151
+ });
152
+
153
+ test("a declaration that shadows the alias is not a usage", () => {
154
+ // Only the declaration is excluded. A read of the shadowing local is a bare
155
+ // reference, and that does count. Real code therefore keeps the import.
156
+ const src = `text := import("text")
157
+
158
+ doSomething := func() {
159
+ text := "local"
160
+ return 1
161
+ }
110
162
  `;
111
163
  expect(unusedAliases(src)).toEqual(["text"]);
112
164
  });
113
165
 
166
+ test("the alias as a member of another value still counts as used", () => {
167
+ // The check is wide on purpose. It never reports an import that the
168
+ // previous predicate kept.
169
+ const src = `text := import("text")
170
+
171
+ doSomething := func(opts) {
172
+ return opts.text
173
+ }
174
+ `;
175
+ expect(unusedAliases(src)).toEqual([]);
176
+ });
177
+
178
+ test("a longer name that starts with the alias is not a usage", () => {
179
+ const src = `ll := import(":ll")
180
+
181
+ doSomething := func() {
182
+ return llExtra(1)
183
+ }
184
+ `;
185
+ expect(unusedAliases(src)).toEqual(["ll"]);
186
+ });
187
+
114
188
  test("reports the line of each import", () => {
115
189
  const src = `ll := import(":ll")
116
190
  sets := import(":sets")
@@ -32,14 +32,26 @@ function escapeForRegExp(s: string): string {
32
32
  /**
33
33
  * Find imports the source does not use.
34
34
  *
35
- * An import is used when its alias appears with a dot somewhere else in the
36
- * code: 'text.split(...)'. A module passed around as a value, say
37
- * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an
38
- * import the source no longer needs is nothing, while the cost of dropping one
39
- * it does need is a broken build.
35
+ * An import counts as used when its alias appears elsewhere in the code as a
36
+ * name of its own. Three shapes count:
40
37
  *
41
- * Throws when the source does not parse, with the same line context the
42
- * compiler reports.
38
+ * - dereferenced: 'text.split(x)'
39
+ * - called: 'doIt(x)', for a lib that exports a bare function
40
+ * - passed as a value: 'myFunc(text)'
41
+ *
42
+ * Two shapes do not count. A longer name that ends with the alias ('xtext') is
43
+ * a different name. A declaration that shadows the alias ('text := ...') binds
44
+ * the name. It does not read the import.
45
+ *
46
+ * Every other occurrence counts. This includes a member of another value
47
+ * ('opts.text') and text inside a string literal ('"a text file"'). The check
48
+ * is wide on purpose. An import that the source no longer needs costs nothing.
49
+ * A dropped import that the source needs breaks the build. The predicate must
50
+ * stay wider than every earlier predicate. A release then reports fewer unused
51
+ * imports, and never more.
52
+ *
53
+ * Throws when the source does not parse. The error carries the same line
54
+ * context that the compiler reports.
43
55
  */
44
56
  export function findUnusedImports(
45
57
  logger: MiLogger,
@@ -75,7 +87,7 @@ export function findUnusedImports(
75
87
  }
76
88
 
77
89
  return imports.filter((im) => {
78
- const usageRE = new RegExp(`\\b${escapeForRegExp(im.alias)}\\s*\\.`);
90
+ const usageRE = new RegExp(`(?<!\\w)${escapeForRegExp(im.alias)}\\b(?!\\s*:=)`);
79
91
  return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));
80
92
  });
81
93
  }