@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
|
|
20
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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(
|
|
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
|
|
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"}
|
package/dist/compiler/imports.js
CHANGED
|
@@ -16,14 +16,26 @@ function escapeForRegExp(s) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Find imports the source does not use.
|
|
18
18
|
*
|
|
19
|
-
* An import
|
|
20
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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(
|
|
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
|
|
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.
|
|
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/
|
|
21
|
-
"@milaboratories/
|
|
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-
|
|
30
|
-
"@milaboratories/ts-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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")
|
package/src/compiler/imports.ts
CHANGED
|
@@ -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
|
|
36
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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(
|
|
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
|
}
|