Skip to content

Commit 4aa3b88

Browse files
committed
done
1 parent 77ff1bc commit 4aa3b88

File tree

5 files changed

+332
-168
lines changed

5 files changed

+332
-168
lines changed

packages/cli/src/helper/file.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,26 @@ export function removeIndexesFromSourceCode(
1616
) {
1717
let newSourceCode = sourceCode;
1818

19-
// sort to start removing from the of the file end
20-
indexesToRemove.sort((a, b) => b.startIndex - a.startIndex);
19+
// Sort the indexes based on startIndex (ascending)
20+
indexesToRemove.sort((a, b) => a.startIndex - b.startIndex);
21+
22+
// Merge overlapping or contiguous ranges
23+
const mergedIndexes: { startIndex: number; endIndex: number }[] = [];
24+
for (const range of indexesToRemove) {
25+
const last = mergedIndexes[mergedIndexes.length - 1];
26+
if (last && range.startIndex <= last.endIndex) {
27+
// Merge the current range with the last range
28+
last.endIndex = Math.max(last.endIndex, range.endIndex);
29+
} else {
30+
// Add as a new range
31+
mergedIndexes.push({ ...range });
32+
}
33+
}
2134

22-
// TODO improve this method by implementing merging of indexes to prevent accidental deletion of code
35+
// Sort to start removing from the end of the file
36+
mergedIndexes.sort((a, b) => b.startIndex - a.startIndex);
2337

24-
indexesToRemove.forEach(({ startIndex, endIndex }) => {
38+
mergedIndexes.forEach(({ startIndex, endIndex }) => {
2539
newSourceCode =
2640
newSourceCode.slice(0, startIndex) + newSourceCode.slice(endIndex);
2741
});

packages/cli/src/languagesPlugins/javascript.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -750,10 +750,10 @@ foofoo();
750750
foobar();
751751
`,
752752
expectedSourceCode: `
753-
import foo, { a, c as c_n, } from "./export1";
753+
import foo, { a, c as c_n, z } from "./export1";
754754
import barbar from "./export2";
755-
756-
755+
import foofoo from "./export3";
756+
import foobar from "./export4";
757757
758758
foo();
759759
a();
@@ -781,10 +781,10 @@ let foofoo: FooFoo;
781781
let foobar: FooBar;
782782
`,
783783
expectedSourceCode: `
784-
import Foo, { A, C as C_N, } from "./export1";
784+
import Foo, { A, C as C_N, Z } from "./export1";
785785
import BarBar from "./export2";
786-
787-
786+
import FooFoo from "./export3";
787+
import FooBar from "./export4";
788788
789789
let foo: Foo;
790790
let a: A;

packages/cli/src/languagesPlugins/javascript.ts

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@ class JavascriptPlugin implements LanguagePlugin {
5959
return;
6060
}
6161

62-
// remove the other annotations
63-
const nextNode = node.nextNamedSibling;
62+
let nextNode = node.nextNamedSibling;
63+
// We need to remove all decorators too
64+
while (nextNode && nextNode.type === "decorator") {
65+
nextNode = nextNode.nextNamedSibling;
66+
}
67+
6468
if (!nextNode) {
6569
throw new Error("Could not find next node");
6670
}
@@ -475,14 +479,8 @@ class JavascriptPlugin implements LanguagePlugin {
475479
return depExports;
476480
}
477481

478-
// TODO thought, we could leverage LLM to better find the usages of the import identifiers
479-
// This is really a case by case issue here. This implementation only works for some cases
480-
#getImportIdentifiersUsages(
481-
node: Parser.SyntaxNode,
482-
identifier: Parser.SyntaxNode,
483-
) {
484-
let usageNodes: Parser.SyntaxNode[] = [];
485-
const identifierQuery = new Parser.Query(
482+
#getIdentifiersNode(node: Parser.SyntaxNode, identifier: Parser.SyntaxNode) {
483+
const query = new Parser.Query(
486484
this.parser.getLanguage(),
487485
this.isTypescript
488486
? `
@@ -499,13 +497,26 @@ class JavascriptPlugin implements LanguagePlugin {
499497
`,
500498
);
501499

502-
const identifierCaptures = identifierQuery.captures(node);
503-
identifierCaptures.forEach((capture) => {
504-
if (capture.node.id === identifier.id) {
505-
return;
506-
}
500+
const captures = query.captures(node);
501+
const nodes = captures.map((capture) => capture.node);
502+
503+
const identifiers = nodes.filter((node) => node.id !== identifier.id);
504+
505+
return identifiers;
506+
}
507+
508+
// TODO We could leverage LLM to better find the usages of the import identifiers
509+
// This is really a case by case issue here. This implementation only works for some cases
510+
#getImportIdentifiersUsages(
511+
node: Parser.SyntaxNode,
512+
identifier: Parser.SyntaxNode,
513+
) {
514+
const otherIdentifiers = this.#getIdentifiersNode(node, identifier);
515+
516+
const usageNodes: Parser.SyntaxNode[] = [];
507517

508-
let targetNode = capture.node;
518+
otherIdentifiers.forEach((otherIdentifier) => {
519+
let targetNode = otherIdentifier;
509520
while (true) {
510521
// we can remove from the array
511522
if (targetNode.parent && targetNode.parent.type === "array") {
@@ -530,10 +541,6 @@ class JavascriptPlugin implements LanguagePlugin {
530541
return usageNodes.push(targetNode);
531542
});
532543

533-
usageNodes = usageNodes.filter(
534-
(usageNode) => usageNode.id !== identifier.id,
535-
);
536-
537544
return usageNodes;
538545
}
539546

@@ -544,7 +551,6 @@ class JavascriptPlugin implements LanguagePlugin {
544551
) {
545552
const indexesToRemove: { startIndex: number; endIndex: number }[] = [];
546553

547-
// Parse the source code to get the AST
548554
const tree = this.parser.parse(sourceCode);
549555

550556
// Get all imports from the file
@@ -565,12 +571,6 @@ class JavascriptPlugin implements LanguagePlugin {
565571
const depExport = depExportMap.get(depImport.source);
566572

567573
if (!depExport) {
568-
// If no exports exist for the import source, mark the entire import for removal
569-
indexesToRemove.push({
570-
startIndex: depImport.node.startIndex,
571-
endIndex: depImport.node.endIndex,
572-
});
573-
574574
// Remove usages of all identifiers from this import
575575
depImport.identifiers.forEach((importIdentifier) => {
576576
const usageNodes = this.#getImportIdentifiersUsages(
@@ -642,22 +642,6 @@ class JavascriptPlugin implements LanguagePlugin {
642642
});
643643
}
644644
});
645-
646-
if (invalidIdentifiers.length === depImport.identifiers.length) {
647-
// If all identifiers are invalid, mark the entire import for removal
648-
indexesToRemove.push({
649-
startIndex: depImport.node.startIndex,
650-
endIndex: depImport.node.endIndex,
651-
});
652-
} else {
653-
// Remove only the invalid identifiers from the import statement
654-
invalidIdentifiers.forEach((identifierNode) => {
655-
indexesToRemove.push({
656-
startIndex: identifierNode.startIndex,
657-
endIndex: identifierNode.endIndex,
658-
});
659-
});
660-
}
661645
});
662646

663647
// Remove invalid imports and usages from the source code

packages/cli/src/languagesPlugins/python.test.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,7 @@ describe("Should get imports properly", () => {
121121
});
122122

123123
describe("Should get exports properly", () => {
124-
const entryPointPath = "api/index.py";
125-
126-
const plugin = new PythonPlugin(entryPointPath);
124+
const plugin = new PythonPlugin("api/index.py");
127125

128126
it.each([
129127
{
@@ -421,3 +419,113 @@ def bar(b):
421419
expect(exports[1].identifiers[0].identifierNode.text).toBe(`bar`);
422420
});
423421
});
422+
423+
describe("Should cleanup unused imports", () => {
424+
const entryPoint = "api/index.py";
425+
const plugin = new PythonPlugin(entryPoint);
426+
427+
it.each([
428+
{
429+
sourceCode: `from module import foo`,
430+
expectedSourceCode: ``,
431+
},
432+
{
433+
sourceCode: `from module import foo as bar`,
434+
expectedSourceCode: ``,
435+
},
436+
{
437+
sourceCode: `from module import foo`,
438+
expectedSourceCode: ``,
439+
},
440+
{
441+
sourceCode: `from module import foo, bar as b`,
442+
expectedSourceCode: ``,
443+
},
444+
])(
445+
"Should completely remove unused imports",
446+
({ sourceCode, expectedSourceCode }) => {
447+
const updatedSourceCode = plugin.cleanupUnusedImports(
448+
entryPoint,
449+
sourceCode,
450+
);
451+
452+
expect(updatedSourceCode).toBe(expectedSourceCode);
453+
},
454+
);
455+
456+
it("Should retain side effect import", () => {
457+
const sourceCode = `import module`;
458+
459+
const updatedSourceCode = plugin.cleanupUnusedImports(
460+
entryPoint,
461+
sourceCode,
462+
);
463+
464+
expect(updatedSourceCode).toBe(sourceCode);
465+
});
466+
467+
it.each([
468+
{
469+
sourceCode: `
470+
from module import foo, bar
471+
bar()
472+
`,
473+
expectedSourceCode: `
474+
from module import , bar
475+
bar()
476+
`,
477+
},
478+
{
479+
sourceCode: `
480+
from module import foo, bar
481+
foo()
482+
`,
483+
expectedSourceCode: `
484+
from module import foo,
485+
foo()
486+
`,
487+
},
488+
])(
489+
"Should remove partially unused imports",
490+
({ sourceCode, expectedSourceCode }) => {
491+
const updatedSourceCode = plugin.cleanupUnusedImports(
492+
entryPoint,
493+
sourceCode,
494+
);
495+
496+
expect(updatedSourceCode).toBe(expectedSourceCode);
497+
},
498+
);
499+
it.each([
500+
{
501+
sourceCode: `
502+
from module import foo as f, bar as b
503+
b()
504+
`,
505+
expectedSourceCode: `
506+
from module import , bar as b
507+
b()
508+
`,
509+
},
510+
{
511+
sourceCode: `
512+
from module import foo as f, bar as b
513+
f()
514+
`,
515+
expectedSourceCode: `
516+
from module import foo as f,
517+
f()
518+
`,
519+
},
520+
])(
521+
"Should remove partially unused imports with aliases",
522+
({ sourceCode, expectedSourceCode }) => {
523+
const updatedSourceCode = plugin.cleanupUnusedImports(
524+
entryPoint,
525+
sourceCode,
526+
);
527+
528+
expect(updatedSourceCode).toBe(expectedSourceCode);
529+
},
530+
);
531+
});

0 commit comments

Comments
 (0)