Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generators/bootstrap/support/java-lint-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { removeUnusedImports } from 'java-lint';
import { removeUnusedImports } from '../../java/support/index.ts';

export default ({ fileContents }: { fileContents: string }): { result: string } | { error: string } => {
try {
Expand Down
1 change: 1 addition & 0 deletions generators/java/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export { default as packageInfoTransform } from './package-info-transform.ts';
export * from './package-info-transform.ts';
export * from './prepare-entity.ts';
export * from './reserved-keywords.ts';
export { removeUnusedImports } from './unused-imports.ts';
export * from './util.ts';
140 changes: 140 additions & 0 deletions generators/java/support/unused-imports.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, expect, it } from 'esmocha';

import { parse } from 'java-parser';

import { collectGlobalIdentifiersNodes, removeUnusedImports } from './unused-imports.ts';

const source = `package my.java.project;

import java.util.*;

import project.Used1;
import project.Unused1;
import project.Used2;
import project.Unused2;

public class HelloWorldExample {
public static void main(Used1 args[]) {
List<Used2> arguments = java.util.Arrays.asList(args);
System.out.println("Arguments:");
System.out.println(arguments);
}
}
`;

describe('java-lint', () => {
it('collectGlobalIdentifiersNodes', () => {
expect(collectGlobalIdentifiersNodes(parse(source))).toMatchInlineSnapshot(`
[
"HelloWorldExample",
"main",
"args",
"Used1",
"arguments",
"List",
"Used2",
"System",
"out",
"println",
"java",
"util",
"Arrays",
"asList",
]
`);
});
describe('removeUnusedImports', () => {
it('should remove unused imports', () => {
expect(removeUnusedImports(source)).toMatchInlineSnapshot(`
"package my.java.project;

import java.util.*;

import project.Used1;
import project.Used2;

public class HelloWorldExample {
public static void main(Used1 args[]) {
List<Used2> arguments = java.util.Arrays.asList(args);
System.out.println("Arguments:");
System.out.println(arguments);
}
}
"
`);
});

it('should remove same package imports', () => {
expect(
removeUnusedImports(`package my.java.project;

import my.java.project.Used1;
import my.java.project.Used2;

public class HelloWorldExample {
public static void main(Used1 args[]) {
List<Used2> arguments = java.util.Arrays.asList(args);
System.out.println("Arguments:");
System.out.println(arguments);
}
}
`),
).toMatchInlineSnapshot(`
"package my.java.project;


public class HelloWorldExample {
public static void main(Used1 args[]) {
List<Used2> arguments = java.util.Arrays.asList(args);
System.out.println("Arguments:");
System.out.println(arguments);
}
}
"
`);
});

it('should not fail with emptyStatement', () => {
expect(
removeUnusedImports(`package my.java.project;

import my.java.project.Used1;;

public class HelloWorldExample {}
`),
).toMatchInlineSnapshot(`
"package my.java.project;
;

public class HelloWorldExample {}
"
`);
});

it('should not remove import static when', () => {
expect(
removeUnusedImports(`package my.java.project;

import static org.mockito.Mockito.when;

public class HelloWorldExample {
public static void main() {
when();
}
}
`),
).toMatchInlineSnapshot(`
"package my.java.project;

import static org.mockito.Mockito.when;

public class HelloWorldExample {
public static void main() {
when();
}
}
"
`);
});
});
});
79 changes: 79 additions & 0 deletions generators/java/support/unused-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { type CstElement, type CstNode, parse } from 'java-parser';

const skippedTypes = ['packageDeclaration', 'importDeclaration'];

/**
* Lazy implementation of used global identifiers collector.
* @param cstNode
* @returns
*/
export const collectGlobalIdentifiersNodes = (cstNode: CstNode): string[] => {
const nodes = [cstNode];
const identifiers: Set<string> = new Set();

for (let node; (node = nodes.shift()); ) {
for (const identifier of Object.keys(node.children)) {
node.children[identifier]
.filter((element: CstElement) => !(element as any).name || !skippedTypes.includes((element as any).name))
.forEach((element: CstElement) => {
if ('image' in element) {
if (element.image) {
const tokenTypes = [element.tokenType];
if (Array.isArray(element.tokenType.LONGER_ALT)) {
tokenTypes.push(...element.tokenType.LONGER_ALT);
} else if (element.tokenType.LONGER_ALT) {
tokenTypes.push(element.tokenType.LONGER_ALT);
}
if (tokenTypes.some(({ name, isParent }) => name === 'Identifier' && isParent)) {
const categories = tokenTypes.map(({ CATEGORIES }) => CATEGORIES).flat();
if (!categories.some(cat => cat?.name === 'Keyword')) {
identifiers.add(element.image);
}
}
}
} else {
nodes.push(element);
}
});
}
}
return [...identifiers];
};

export const removeUnusedImports = (content: string) => {
const cstNode = parse(content);
const importDeclarationNodes: any[] = (cstNode.children.ordinaryCompilationUnit[0] as any).children.importDeclaration;
if (!importDeclarationNodes) {
return content;
}
const filePackage = (cstNode.children.ordinaryCompilationUnit[0] as any).children.packageDeclaration[0].children.Identifier.map(
(identifier: any) => identifier.image,
).join('.');
const identifiers = collectGlobalIdentifiersNodes(cstNode);
const unusedImportNodes: any[] = importDeclarationNodes
.filter(importDec => !importDec.children.Star && !importDec.children.emptyStatement)
.map(imp => {
const packageOrTypeName = imp.children.packageOrTypeName[0];
return [packageOrTypeName.children.Identifier[packageOrTypeName.children.Identifier.length - 1].image, imp];
})
.filter(
([identifier, importDec]) =>
!identifiers.includes(identifier) ||
importDec.children.packageOrTypeName[0].children.Identifier.map((el: any) => el.image)
.slice(0, -1)
.join('.') === filePackage,
)
.map(([_identifier, impNode]) => impNode);

// Reverse
unusedImportNodes.sort((a, b) => b.location.startOffset - a.location.startOffset);

for (const unusedImport of unusedImportNodes) {
let { startOffset } = unusedImport.location;
if (content.charAt(startOffset - 1) === '\n') {
startOffset--;
}
content = `${content.slice(0, startOffset)}${content.slice(unusedImport.location.endOffset + 1)}`;
}
return content;
};
106 changes: 4 additions & 102 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"fast-xml-parser": "5.5.6",
"globals": "17.4.0",
"isbinaryfile": "5.0.0",
"java-lint": "0.3.0",
"java-parser": "3.0.1",
"latest-version": "9.0.0",
"lodash-es": "4.17.23",
"mem-fs": "4.1.4",
Expand Down
Loading