From 047cdb5cfbc5d394f2a5a2f94c2a5eb58252ac7e Mon Sep 17 00:00:00 2001 From: MaoXiaodong Date: Fri, 17 Jul 2026 21:34:03 +0800 Subject: [PATCH 1/4] ci: add GitHub Actions workflow to publish extension to marketplaces and bump package version to 1.0.3 --- .github/workflows/publish.yml | 6 ------ package.json | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9a2a16d..2b38094 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,12 +20,6 @@ jobs: node-version: 20 cache: 'npm' - - name: Sync Version from Tag - if: github.ref_type == 'tag' - run: | - VERSION=${GITHUB_REF_NAME#v} - npm version $VERSION --no-git-tag-version - - name: Install, Compile & Package run: | npm install diff --git a/package.json b/package.json index b931ee4..2ceef1d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Java Sorter (Eclipse Style)", "description": "%extension.description%", "icon": "icon.png", - "version": "1.0.2", + "version": "1.0.3", "publisher": "eoasmxd", "repository": { "type": "git", From d5c04198be53904cf6bcad18401adef01d03b234 Mon Sep 17 00:00:00 2001 From: MaoXiaodong Date: Sat, 18 Jul 2026 13:28:31 +0800 Subject: [PATCH 2/4] feat: implement member sorting logic, unit testing infrastructure, and CI workflows --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 28 +++ .vscodeignore | 4 + package-lock.json | 4 +- package.json | 3 +- package.nls.zh-cn.json | 2 +- package.nls.zh.json | 2 +- src/parser/antlr-parser.ts | 208 +++++++++++++----- src/sorter/member-sorter.ts | 16 ++ src/test/run-tests.ts | 63 ++++++ src/types.ts | 4 +- .../ComprehensiveClassTest.expected.java | 53 +++++ .../ComprehensiveInterfaceTest.expected.java | 15 ++ .../original/ComprehensiveClassTest.java | 53 +++++ .../original/ComprehensiveInterfaceTest.java | 15 ++ 15 files changed, 411 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 src/test/run-tests.ts create mode 100644 test/fixtures/expected/ComprehensiveClassTest.expected.java create mode 100644 test/fixtures/expected/ComprehensiveInterfaceTest.expected.java create mode 100644 test/fixtures/original/ComprehensiveClassTest.java create mode 100644 test/fixtures/original/ComprehensiveInterfaceTest.java diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2b38094..9fbcbba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: - name: Install, Compile & Package run: | npm install - npm run compile + npm run test:unit npx @vscode/vsce package - name: Publish to VS Code Marketplace diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..060b3f1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Run Unit Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install & Run Tests + run: | + npm install + npm run test:unit diff --git a/.vscodeignore b/.vscodeignore index 1f0b539..f74460f 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -6,3 +6,7 @@ tsconfig.json **/*.ts !**/*.d.ts .github/** +*.vsix +test/** +out/test/** + diff --git a/package-lock.json b/package-lock.json index 97a6b2e..9701f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-java-sorter", - "version": "1.0.0", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-java-sorter", - "version": "1.0.0", + "version": "1.0.3", "dependencies": { "java-parser": "^2.3.0" }, diff --git a/package.json b/package.json index 2ceef1d..7f6b2df 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "clean": "node -e \"try { require('fs').rmSync('out', {recursive:true,force:true}); require('fs').readdirSync('.').filter(f => f.endsWith('.vsix')).forEach(f => require('fs').unlinkSync(f)) } catch(e) {}\"", - "package": "npx @vscode/vsce package" + "package": "npx @vscode/vsce package", + "test:unit": "npm run compile && node out/test/run-tests.js" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index 7bb2dff..67dec96 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -3,5 +3,5 @@ "command.title": "Java Sorter: 排序类成员 (仿 Eclipse 风格)", "config.memberOrder": "指定不同类别成员的物理排列顺序。如果 sortAllMembers 为 false,未包含在数组中的类型将保持原位不动。", "config.visibilityOrder": "指定同组内各成员可见性的排序优先级顺序(如 ['public', 'protected', 'package', 'private'])。保留空数组则不启用可见性排序。", - "config.sortAllMembers": "是否对所有类成员进行排序。若为 false,则字段、枚举常量和初始化块将像钉子一样固定在原位不动。" + "config.sortAllMembers": "是否对所有类成员进行排序。若为 false,则字段、枚举常量和初始化块将保持在原始位置,不参与排序。" } diff --git a/package.nls.zh.json b/package.nls.zh.json index 7bb2dff..67dec96 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -3,5 +3,5 @@ "command.title": "Java Sorter: 排序类成员 (仿 Eclipse 风格)", "config.memberOrder": "指定不同类别成员的物理排列顺序。如果 sortAllMembers 为 false,未包含在数组中的类型将保持原位不动。", "config.visibilityOrder": "指定同组内各成员可见性的排序优先级顺序(如 ['public', 'protected', 'package', 'private'])。保留空数组则不启用可见性排序。", - "config.sortAllMembers": "是否对所有类成员进行排序。若为 false,则字段、枚举常量和初始化块将像钉子一样固定在原位不动。" + "config.sortAllMembers": "是否对所有类成员进行排序。若为 false,则字段、枚举常量和初始化块将保持在原始位置,不参与排序。" } diff --git a/src/parser/antlr-parser.ts b/src/parser/antlr-parser.ts index e22987d..b575271 100644 --- a/src/parser/antlr-parser.ts +++ b/src/parser/antlr-parser.ts @@ -44,7 +44,8 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { private collectClassMembers( classBodyNode: CstNode, className: string, - minOffset: number + minOffset: number, + contextType: "class" | "interface" | "enum" ): MemberInfo[] { const members: MemberInfo[] = []; const bodyChildren = classBodyNode.children || classBodyNode; @@ -65,14 +66,15 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { } } - let startOffset = extractStartOffset(d); + const rawStartOffset = extractStartOffset(d); + let startOffset = rawStartOffset; const endOffset = extractEndOffset(d); startOffset = extendStartOffsetToIncludeComments(this.source, startOffset, minOffset); if (children.fieldDeclaration) { for (const fd of toArray(children.fieldDeclaration)) { - const info = makeFieldInfo(fd as CstNode, this.source, startOffset, endOffset); + const info = makeFieldInfo(fd as CstNode, this.source, startOffset, endOffset, contextType); if (info) { members.push(info); } @@ -82,9 +84,26 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { if (children.methodDeclaration) { for (const md of toArray(children.methodDeclaration)) { - const info = makeMethodInfo(md as CstNode, this.source, startOffset, endOffset, false); - if (info) { - members.push(info); + const mdNode = md as CstNode; + if (isParsedAsMethodButActuallyRecord(this.source, startOffset, endOffset)) { + const recordName = extractMethodName(mdNode); + members.push({ + type: MemberType.NESTED_TYPE, + start: startOffset, + end: endOffset, + name: recordName || "", + visibility: extractVisibility(toArray(mdNode.children?.modifier || []), contextType), + isStatic: extractStatic(toArray(mdNode.children?.modifier || [])), + paramCount: 0, + nestedKind: "record", + isAbstract: false, + fullText: this.source.slice(startOffset, endOffset), + }); + } else { + const info = makeMethodInfo(mdNode, this.source, startOffset, endOffset, false, contextType); + if (info) { + members.push(info); + } } } continue; @@ -93,7 +112,7 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { const cdecl = children.constructorDeclaration || originalChildren.constructorDeclaration; if (cdecl) { for (const cd of toArray(cdecl)) { - const info = makeConstructorInfo(cd as CstNode, this.source, startOffset, endOffset); + const info = makeConstructorInfo(cd as CstNode, this.source, startOffset, endOffset, contextType); if (info) { members.push(info); } @@ -102,8 +121,8 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { } if (children.classDeclaration) { - for (const nd of toArray(children.classDeclaration)) { - const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "class", className); + for (const cd of toArray(children.classDeclaration)) { + const info = makeNestedTypeInfo(cd as CstNode, this.source, startOffset, endOffset, "class", className, contextType); if (info) { members.push(info); } @@ -112,8 +131,8 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { } if (children.interfaceDeclaration) { - for (const nd of toArray(children.interfaceDeclaration)) { - const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "interface", className); + for (const id of toArray(children.interfaceDeclaration)) { + const info = makeNestedTypeInfo(id as CstNode, this.source, startOffset, endOffset, "interface", className, contextType); if (info) { members.push(info); } @@ -123,7 +142,27 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { if (children.enumDeclaration) { for (const nd of toArray(children.enumDeclaration)) { - const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "enum", className); + const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "enum", className, contextType); + if (info) { + members.push(info); + } + } + continue; + } + + if (children.recordDeclaration) { + for (const nd of toArray(children.recordDeclaration)) { + const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "record", className, contextType); + if (info) { + members.push(info); + } + } + continue; + } + + if (children.annotationTypeDeclaration) { + for (const nd of toArray(children.annotationTypeDeclaration)) { + const info = makeNestedTypeInfo(nd as CstNode, this.source, startOffset, endOffset, "annotation", className, contextType); if (info) { members.push(info); } @@ -149,7 +188,9 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { } const blockInit = children.block || originalChildren.block; - if (blockInit) { + const rawDeclText = this.source.slice(rawStartOffset, endOffset).trim(); + const isInstanceBlock = blockInit || (rawDeclText.startsWith("{") && rawDeclText.endsWith("}")); + if (isInstanceBlock) { members.push({ type: MemberType.INITIALIZER, start: startOffset, @@ -252,7 +293,7 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { enumConstants = extractEnumConstants(bodyNode, this.source); } - const ordinaryMembers = this.collectClassMembers(bodyNode, typeName, bodyStartOff); + const ordinaryMembers = this.collectClassMembers(bodyNode, typeName, bodyStartOff, isEnum ? "enum" : "class"); const allMembers = [...enumConstants, ...ordinaryMembers]; this.classes.push({ @@ -294,7 +335,7 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { : fullBodyEnd; const typeName = extractTypeName(node, false); - const members = this.collectClassMembers(bodyNode, typeName, bodyStartOff); + const members = this.collectClassMembers(bodyNode, typeName, bodyStartOff, "interface"); this.classes.push({ name: typeName, @@ -349,9 +390,12 @@ function extractEndOffset(node: CstNode): number { /** * 提取修饰符数组中的可见性类型 */ -function extractVisibility(modifiers: unknown[]): Visibility { +function extractVisibility( + modifiers: unknown[], + contextType: "class" | "interface" | "enum" +): Visibility { if (!modifiers || !Array.isArray(modifiers)) { - return Visibility.PACKAGE; + return contextType === "interface" ? Visibility.PUBLIC : Visibility.PACKAGE; } for (const modifier of modifiers) { const m = modifier as CstNode; @@ -366,7 +410,7 @@ function extractVisibility(modifiers: unknown[]): Visibility { return Visibility.PRIVATE; } } - return Visibility.PACKAGE; + return contextType === "interface" ? Visibility.PUBLIC : Visibility.PACKAGE; } /** @@ -529,7 +573,7 @@ function extractConstructorName(node: CstNode): string { /** * 从方法/构造函数节点中下潜计算形参数量 */ -function extractParamCount(node: CstNode): number { +function extractParamCount(node: CstNode, source: string): number { const children = node.children || node; let declarator: CstNode | undefined; @@ -546,31 +590,48 @@ function extractParamCount(node: CstNode): number { } if (declarator) { - const declChildren = declarator.children || declarator; - if (declChildren.formalParameters) { - const formalParams = toArray(declChildren.formalParameters)[0] as CstNode; - if (formalParams) { - const fpChildren = formalParams.children || formalParams; - if (fpChildren.formalParameterList) { - const paramList = toArray(fpChildren.formalParameterList)[0] as CstNode; - if (paramList) { - const paramListChildren = paramList.children || paramList; - let count = 0; - if (paramListChildren.formalParameter) { - count += toArray(paramListChildren.formalParameter).length; - } - if (paramListChildren.variableArityParameter) { - count += toArray(paramListChildren.variableArityParameter).length; - } - return count; - } - } + const start = extractStartOffset(declarator); + const end = extractEndOffset(declarator); + const text = source.slice(start, end); + + const parenStart = text.indexOf("("); + const parenEnd = text.lastIndexOf(")"); + if (parenStart !== -1 && parenEnd !== -1 && parenEnd > parenStart) { + const paramsText = text.slice(parenStart + 1, parenEnd).trim(); + if (paramsText === "") { + return 0; } + return countOuterCommas(paramsText) + 1; } } return 0; } +/** + * 统计圆括号与尖括号外层的逗号数量 + */ +function countOuterCommas(paramsText: string): number { + let commas = 0; + let angleDepth = 0; + let parenDepth = 0; + + for (let i = 0; i < paramsText.length; i++) { + const char = paramsText[i]; + if (char === "<") { + angleDepth++; + } else if (char === ">") { + angleDepth--; + } else if (char === "(") { + parenDepth++; + } else if (char === ")") { + parenDepth--; + } else if (char === "," && angleDepth === 0 && parenDepth === 0) { + commas++; + } + } + return commas; +} + /** * 构建字段的 MemberInfo 元数据结构 */ @@ -578,11 +639,12 @@ function makeFieldInfo( node: CstNode, source: string, declStart: number, - declEnd: number + declEnd: number, + contextType: "class" | "interface" | "enum" ): MemberInfo | null { const children = node.children || node; const modifiers = toArray(children.modifier || []); - const visibility = extractVisibility(modifiers); + const visibility = extractVisibility(modifiers, contextType); const isStatic = extractStatic(modifiers); const varDeclList = children.variableDeclaratorList; @@ -632,15 +694,16 @@ function makeMethodInfo( source: string, declStart: number, declEnd: number, - isConstructor: boolean + isConstructor: boolean, + contextType: "class" | "interface" | "enum" ): MemberInfo | null { const children = node.children || node; const modifiers = toArray(children.modifier || []); - const visibility = extractVisibility(modifiers); + const visibility = extractVisibility(modifiers, contextType); const isStatic = extractStatic(modifiers); const isAbstract = extractAbstract(modifiers); const name = isConstructor ? extractConstructorName(node) : extractMethodName(node); - const paramCount = extractParamCount(node); + const paramCount = extractParamCount(node, source); return { type: isConstructor ? MemberType.CONSTRUCTOR : MemberType.METHOD, @@ -663,12 +726,13 @@ function makeConstructorInfo( node: CstNode, source: string, declStart: number, - declEnd: number + declEnd: number, + contextType: "class" | "interface" | "enum" ): MemberInfo | null { const children = node.children || node; const modifiers = toArray(children.modifier || []); - const visibility = extractVisibility(modifiers); - const paramCount = extractParamCount(node); + const visibility = extractVisibility(modifiers, contextType); + const paramCount = extractParamCount(node, source); const ctorName = extractConstructorName(node); return { @@ -693,26 +757,29 @@ function makeNestedTypeInfo( source: string, declStart: number, declEnd: number, - kind: "class" | "interface" | "enum", - parentName: string + kind: "class" | "interface" | "enum" | "record" | "annotation", + parentName: string, + contextType: "class" | "interface" | "enum" ): MemberInfo | null { const children = node.children || node; const modifiers = toArray(children.modifier || []); - const visibility = extractVisibility(modifiers); + const visibility = extractVisibility(modifiers, contextType); const isStatic = extractStatic(modifiers); - const typeName = extractTypeName(node, kind === "enum"); + + const text = source.slice(declStart, declEnd); + const details = extractNestedTypeDetails(text); return { type: MemberType.NESTED_TYPE, start: declStart, end: declEnd, - name: typeName, + name: details.name, visibility, isStatic, paramCount: 0, - nestedKind: kind, + nestedKind: details.kind, isAbstract: false, - fullText: source.slice(declStart, declEnd), + fullText: text, }; } @@ -817,3 +884,38 @@ function extendStartOffsetToIncludeComments( } return current; } + +/** + * 判断文本段是否是 Record 声明 + */ +function isParsedAsMethodButActuallyRecord(source: string, declStart: number, declEnd: number): boolean { + const text = source.slice(declStart, declEnd); + return /\brecord\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(text); +} + +/** + * 从嵌套声明文本中提取类型种类与名称 + */ +function extractNestedTypeDetails(text: string): { kind: "class" | "interface" | "enum" | "record" | "annotation"; name: string } { + const annotationMatch = /@interface\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(text); + if (annotationMatch) { + return { kind: "annotation", name: annotationMatch[1] }; + } + const interfaceMatch = /\binterface\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(text); + if (interfaceMatch) { + return { kind: "interface", name: interfaceMatch[1] }; + } + const enumMatch = /\benum\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(text); + if (enumMatch) { + return { kind: "enum", name: enumMatch[1] }; + } + const recordMatch = /\brecord\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(text); + if (recordMatch) { + return { kind: "record", name: recordMatch[1] }; + } + const classMatch = /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(text); + if (classMatch) { + return { kind: "class", name: classMatch[1] }; + } + return { kind: "class", name: "" }; +} diff --git a/src/sorter/member-sorter.ts b/src/sorter/member-sorter.ts index 4001612..1c1acbf 100644 --- a/src/sorter/member-sorter.ts +++ b/src/sorter/member-sorter.ts @@ -70,6 +70,18 @@ function compareMembers(a: MemberInfo, b: MemberInfo, config: SortConfiguration) } if (isMicroSortEnabled(a.type, config)) { + if (a.type !== b.type) { + const typeWeights: Record = { + [MemberType.ENUM_CONSTANT]: 0, + [MemberType.FIELD]: 1, + }; + const weightA = typeWeights[a.type] ?? 99; + const weightB = typeWeights[b.type] ?? 99; + if (weightA !== weightB) { + return weightA - weightB; + } + } + switch (a.type) { case MemberType.CONSTRUCTOR: return compareConstructors(a, b); @@ -79,6 +91,8 @@ function compareMembers(a: MemberInfo, b: MemberInfo, config: SortConfiguration) return compareFields(a, b, config); case MemberType.NESTED_TYPE: return compareNestedTypes(a, b); + case MemberType.INITIALIZER: + return a.start - b.start; default: return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); } @@ -196,6 +210,8 @@ function compareNestedTypes(a: MemberInfo, b: MemberInfo): number { class: 0, interface: 1, enum: 2, + record: 3, + annotation: 4, }; const weightA = kindWeights[a.nestedKind || "class"] ?? 0; const weightB = kindWeights[b.nestedKind || "class"] ?? 0; diff --git a/src/test/run-tests.ts b/src/test/run-tests.ts new file mode 100644 index 0000000..58e2a19 --- /dev/null +++ b/src/test/run-tests.ts @@ -0,0 +1,63 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as assert from "assert"; +import { sortSourceCode } from "../writer/text-reconstructor"; +import { SortConfiguration } from "../types"; + +// 单元测试总入口 +function runAllTests() { + console.log("🚀 Starting Java Sorter Unit Tests..."); + let passed = 0; + let failed = 0; + + const fixturesDir = path.join(__dirname, "../../test/fixtures"); + + // 用例 1: ComprehensiveClassTest 排序验证 + let originalText = ""; + let config: SortConfiguration = { + memberOrder: ["types", "staticFields", "staticInitializers", "staticMethods", "fields", "initializers", "constructors", "methods"], + visibilityOrder: [], + sortAllMembers: true + }; + try { + originalText = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveClassTest.java"), "utf-8"); + const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveClassTest.expected.java"), "utf-8"); + + const result = sortSourceCode(originalText, config); + assert.strictEqual(result.replace(/\r\n/g, "\n"), expectedText.replace(/\r\n/g, "\n")); + console.log("✅ [PASSED]: ComprehensiveClassTest"); + passed++; + } catch (err) { + console.error("❌ [FAILED]: ComprehensiveClassTest"); + console.error(err); + failed++; + } + + // 用例 2: ComprehensiveInterfaceTest 可见性推导排序验证 + try { + const originalText = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveInterfaceTest.java"), "utf-8"); + const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveInterfaceTest.expected.java"), "utf-8"); + + const config: SortConfiguration = { + memberOrder: ["types", "staticFields", "staticInitializers", "staticMethods", "fields", "initializers", "constructors", "methods"], + visibilityOrder: ["public", "protected", "package", "private"], // 启用可见性排序 + sortAllMembers: true + }; + + const result = sortSourceCode(originalText, config); + assert.strictEqual(result.replace(/\r\n/g, "\n"), expectedText.replace(/\r\n/g, "\n")); + console.log("✅ [PASSED]: ComprehensiveInterfaceTest"); + passed++; + } catch (err) { + console.error("❌ [FAILED]: ComprehensiveInterfaceTest"); + console.error(err); + failed++; + } + + console.log(`\n📊 Test Summary: ${passed} passed, ${failed} failed.`); + if (failed > 0) { + process.exit(1); + } +} + +runAllTests(); diff --git a/src/types.ts b/src/types.ts index 4b2c24a..16ab7ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,8 +44,8 @@ export interface MemberInfo { isStatic: boolean; /** 方法参数数量(仅对构造方法和方法有意义) */ paramCount: number; - /** 嵌套类型的种类(class / interface / enum),仅对 NESTED_TYPE 有意义 */ - nestedKind?: "class" | "interface" | "enum"; + /** 嵌套类型的种类(class / interface / enum / record / annotation),仅对 NESTED_TYPE 有意义 */ + nestedKind?: "class" | "interface" | "enum" | "record" | "annotation"; /** 是否为抽象成员 */ isAbstract: boolean; /** 成员前的完整文本(含注解、Javadoc),用于最终拼接 */ diff --git a/test/fixtures/expected/ComprehensiveClassTest.expected.java b/test/fixtures/expected/ComprehensiveClassTest.expected.java new file mode 100644 index 0000000..ff6976d --- /dev/null +++ b/test/fixtures/expected/ComprehensiveClassTest.expected.java @@ -0,0 +1,53 @@ +package test; + +public class ComprehensiveClassTest { + + // 8. 嵌套 Enum + public enum MyEnum { + RED, GREEN, BLUE + } + + // 2. 嵌套 Record + public record NestedRecord(String name, int age) {} + + // 6. 嵌套注解 + public @interface NestedAnnotation { + String value() default ""; + } + + // 1. 静态初始化块 A + static { + System.out.println("Static Initializer A"); + } + + // 7. 静态初始化块 B + static { + System.out.println("Static Initializer B"); + } + + // 3. 私有字段 + private int privateField; + + // 4. 公有字段 + public String publicField; + + // 5. 实例初始化块 A + { + System.out.println("Instance Initializer A"); + } + + // 11. 实例初始化块 B + { + System.out.println("Instance Initializer B"); + } + + // 10. 构造方法 A (单参数) + public ComprehensiveClassTest(int x) { + this.privateField = x; + } + + // 9. 构造方法 B (双参数) + public ComprehensiveClassTest(int x, int y) { + this.privateField = x + y; + } +} diff --git a/test/fixtures/expected/ComprehensiveInterfaceTest.expected.java b/test/fixtures/expected/ComprehensiveInterfaceTest.expected.java new file mode 100644 index 0000000..4423998 --- /dev/null +++ b/test/fixtures/expected/ComprehensiveInterfaceTest.expected.java @@ -0,0 +1,15 @@ +package test; + +public interface ComprehensiveInterfaceTest { + + // 1. 显式 public 方法 + public void explicitPublicMethod(); + + // 2. 隐式默认 public 方法 + void implicitDefaultMethod(); + + // 3. 显式 private 方法 + private void privateHelperMethod() { + // 私有方法 + } +} diff --git a/test/fixtures/original/ComprehensiveClassTest.java b/test/fixtures/original/ComprehensiveClassTest.java new file mode 100644 index 0000000..e9c0d58 --- /dev/null +++ b/test/fixtures/original/ComprehensiveClassTest.java @@ -0,0 +1,53 @@ +package test; + +public class ComprehensiveClassTest { + + // 1. 静态初始化块 A + static { + System.out.println("Static Initializer A"); + } + + // 2. 嵌套 Record + public record NestedRecord(String name, int age) {} + + // 3. 私有字段 + private int privateField; + + // 4. 公有字段 + public String publicField; + + // 5. 实例初始化块 A + { + System.out.println("Instance Initializer A"); + } + + // 6. 嵌套注解 + public @interface NestedAnnotation { + String value() default ""; + } + + // 7. 静态初始化块 B + static { + System.out.println("Static Initializer B"); + } + + // 8. 嵌套 Enum + public enum MyEnum { + RED, GREEN, BLUE + } + + // 9. 构造方法 B (双参数) + public ComprehensiveClassTest(int x, int y) { + this.privateField = x + y; + } + + // 10. 构造方法 A (单参数) + public ComprehensiveClassTest(int x) { + this.privateField = x; + } + + // 11. 实例初始化块 B + { + System.out.println("Instance Initializer B"); + } +} diff --git a/test/fixtures/original/ComprehensiveInterfaceTest.java b/test/fixtures/original/ComprehensiveInterfaceTest.java new file mode 100644 index 0000000..4423998 --- /dev/null +++ b/test/fixtures/original/ComprehensiveInterfaceTest.java @@ -0,0 +1,15 @@ +package test; + +public interface ComprehensiveInterfaceTest { + + // 1. 显式 public 方法 + public void explicitPublicMethod(); + + // 2. 隐式默认 public 方法 + void implicitDefaultMethod(); + + // 3. 显式 private 方法 + private void privateHelperMethod() { + // 私有方法 + } +} From 5885ab14f2a14e07e1be726215c624e391486766 Mon Sep 17 00:00:00 2001 From: MaoXiaodong Date: Sat, 18 Jul 2026 13:38:39 +0800 Subject: [PATCH 3/4] test: add comprehensive unit test runner and fixture files for class and interface sorting --- src/test/run-tests.ts | 4 ++-- ...pected.java => ComprehensiveClassTest.java} | 18 +++++++++--------- ...ed.java => ComprehensiveInterfaceTest.java} | 0 .../original/ComprehensiveClassTest.java | 18 +++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) rename test/fixtures/expected/{ComprehensiveClassTest.expected.java => ComprehensiveClassTest.java} (76%) rename test/fixtures/expected/{ComprehensiveInterfaceTest.expected.java => ComprehensiveInterfaceTest.java} (100%) diff --git a/src/test/run-tests.ts b/src/test/run-tests.ts index 58e2a19..a4cea2d 100644 --- a/src/test/run-tests.ts +++ b/src/test/run-tests.ts @@ -21,7 +21,7 @@ function runAllTests() { }; try { originalText = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveClassTest.java"), "utf-8"); - const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveClassTest.expected.java"), "utf-8"); + const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveClassTest.java"), "utf-8"); const result = sortSourceCode(originalText, config); assert.strictEqual(result.replace(/\r\n/g, "\n"), expectedText.replace(/\r\n/g, "\n")); @@ -36,7 +36,7 @@ function runAllTests() { // 用例 2: ComprehensiveInterfaceTest 可见性推导排序验证 try { const originalText = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveInterfaceTest.java"), "utf-8"); - const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveInterfaceTest.expected.java"), "utf-8"); + const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveInterfaceTest.java"), "utf-8"); const config: SortConfiguration = { memberOrder: ["types", "staticFields", "staticInitializers", "staticMethods", "fields", "initializers", "constructors", "methods"], diff --git a/test/fixtures/expected/ComprehensiveClassTest.expected.java b/test/fixtures/expected/ComprehensiveClassTest.java similarity index 76% rename from test/fixtures/expected/ComprehensiveClassTest.expected.java rename to test/fixtures/expected/ComprehensiveClassTest.java index ff6976d..7cc483a 100644 --- a/test/fixtures/expected/ComprehensiveClassTest.expected.java +++ b/test/fixtures/expected/ComprehensiveClassTest.java @@ -2,7 +2,7 @@ public class ComprehensiveClassTest { - // 8. 嵌套 Enum + // 1. 嵌套 Enum public enum MyEnum { RED, GREEN, BLUE } @@ -10,33 +10,33 @@ public enum MyEnum { // 2. 嵌套 Record public record NestedRecord(String name, int age) {} - // 6. 嵌套注解 + // 3. 嵌套注解 public @interface NestedAnnotation { String value() default ""; } - // 1. 静态初始化块 A + // 4. 静态初始化块 A static { System.out.println("Static Initializer A"); } - // 7. 静态初始化块 B + // 5. 静态初始化块 B static { System.out.println("Static Initializer B"); } - // 3. 私有字段 + // 6. 私有字段 private int privateField; - // 4. 公有字段 + // 7. 公有字段 public String publicField; - // 5. 实例初始化块 A + // 8. 实例初始化块 A { System.out.println("Instance Initializer A"); } - // 11. 实例初始化块 B + // 9. 实例初始化块 B { System.out.println("Instance Initializer B"); } @@ -46,7 +46,7 @@ public ComprehensiveClassTest(int x) { this.privateField = x; } - // 9. 构造方法 B (双参数) + // 11. 构造方法 B (双参数) public ComprehensiveClassTest(int x, int y) { this.privateField = x + y; } diff --git a/test/fixtures/expected/ComprehensiveInterfaceTest.expected.java b/test/fixtures/expected/ComprehensiveInterfaceTest.java similarity index 100% rename from test/fixtures/expected/ComprehensiveInterfaceTest.expected.java rename to test/fixtures/expected/ComprehensiveInterfaceTest.java diff --git a/test/fixtures/original/ComprehensiveClassTest.java b/test/fixtures/original/ComprehensiveClassTest.java index e9c0d58..70be842 100644 --- a/test/fixtures/original/ComprehensiveClassTest.java +++ b/test/fixtures/original/ComprehensiveClassTest.java @@ -2,7 +2,7 @@ public class ComprehensiveClassTest { - // 1. 静态初始化块 A + // 4. 静态初始化块 A static { System.out.println("Static Initializer A"); } @@ -10,33 +10,33 @@ public class ComprehensiveClassTest { // 2. 嵌套 Record public record NestedRecord(String name, int age) {} - // 3. 私有字段 + // 6. 私有字段 private int privateField; - // 4. 公有字段 + // 7. 公有字段 public String publicField; - // 5. 实例初始化块 A + // 8. 实例初始化块 A { System.out.println("Instance Initializer A"); } - // 6. 嵌套注解 + // 3. 嵌套注解 public @interface NestedAnnotation { String value() default ""; } - // 7. 静态初始化块 B + // 5. 静态初始化块 B static { System.out.println("Static Initializer B"); } - // 8. 嵌套 Enum + // 1. 嵌套 Enum public enum MyEnum { RED, GREEN, BLUE } - // 9. 构造方法 B (双参数) + // 11. 构造方法 B (双参数) public ComprehensiveClassTest(int x, int y) { this.privateField = x + y; } @@ -46,7 +46,7 @@ public ComprehensiveClassTest(int x) { this.privateField = x; } - // 11. 实例初始化块 B + // 9. 实例初始化块 B { System.out.println("Instance Initializer B"); } From 94edbb29a8ad66717baac6df7b6a57dbef65de7b Mon Sep 17 00:00:00 2001 From: MaoXiaodong Date: Sat, 18 Jul 2026 13:55:48 +0800 Subject: [PATCH 4/4] feat: implement Java class member metadata parser using antlr-parser and add comprehensive test infrastructure --- .vscode/settings.json | 9 +++++++++ src/parser/antlr-parser.ts | 7 ++++--- src/test/run-tests.ts | 16 ++++++++++++++-- .../expected/ComprehensiveClassTest.java | 4 +++- .../expected/ComprehensiveInterfaceTest.java | 5 ----- .../original/ComprehensiveClassTest.java | 4 +++- .../original/ComprehensiveInterfaceTest.java | 9 ++------- 7 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..808e40b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "java.project.excludes": [ + "**/node_modules", + "**/.metadata", + "**/archetype", + "**/bin", + "**/test/fixtures" + ] +} diff --git a/src/parser/antlr-parser.ts b/src/parser/antlr-parser.ts index b575271..862568d 100644 --- a/src/parser/antlr-parser.ts +++ b/src/parser/antlr-parser.ts @@ -49,7 +49,7 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { ): MemberInfo[] { const members: MemberInfo[] = []; const bodyChildren = classBodyNode.children || classBodyNode; - const declarations = bodyChildren.classBodyDeclaration; + const declarations = bodyChildren.classBodyDeclaration || bodyChildren.interfaceMemberDeclaration; if (!declarations) { return members; } @@ -82,8 +82,9 @@ class ClassBodyVisitor extends BaseJavaCstVisitorWithDefaults { continue; } - if (children.methodDeclaration) { - for (const md of toArray(children.methodDeclaration)) { + const mdecl = children.methodDeclaration || children.interfaceMethodDeclaration; + if (mdecl) { + for (const md of toArray(mdecl)) { const mdNode = md as CstNode; if (isParsedAsMethodButActuallyRecord(this.source, startOffset, endOffset)) { const recordName = extractMethodName(mdNode); diff --git a/src/test/run-tests.ts b/src/test/run-tests.ts index a4cea2d..43bdd85 100644 --- a/src/test/run-tests.ts +++ b/src/test/run-tests.ts @@ -34,8 +34,9 @@ function runAllTests() { } // 用例 2: ComprehensiveInterfaceTest 可见性推导排序验证 + let originalText2 = ""; try { - const originalText = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveInterfaceTest.java"), "utf-8"); + originalText2 = fs.readFileSync(path.join(fixturesDir, "original/ComprehensiveInterfaceTest.java"), "utf-8"); const expectedText = fs.readFileSync(path.join(fixturesDir, "expected/ComprehensiveInterfaceTest.java"), "utf-8"); const config: SortConfiguration = { @@ -44,12 +45,23 @@ function runAllTests() { sortAllMembers: true }; - const result = sortSourceCode(originalText, config); + const result = sortSourceCode(originalText2, config); assert.strictEqual(result.replace(/\r\n/g, "\n"), expectedText.replace(/\r\n/g, "\n")); console.log("✅ [PASSED]: ComprehensiveInterfaceTest"); passed++; } catch (err) { console.error("❌ [FAILED]: ComprehensiveInterfaceTest"); + try { + const { parseJavaClasses } = require("../parser"); + const parsed = parseJavaClasses(originalText2); + if (parsed && parsed[0]) { + console.log("\n=== Interface Debug Members Info ==="); + parsed[0].members.forEach((m: any) => { + console.log(`- Name: "${m.name}", Type: ${m.type}, visibility: ${m.visibility}, paramCount: ${m.paramCount}`); + }); + console.log("====================================\n"); + } + } catch (e) {} console.error(err); failed++; } diff --git a/test/fixtures/expected/ComprehensiveClassTest.java b/test/fixtures/expected/ComprehensiveClassTest.java index 7cc483a..cd0665e 100644 --- a/test/fixtures/expected/ComprehensiveClassTest.java +++ b/test/fixtures/expected/ComprehensiveClassTest.java @@ -4,7 +4,9 @@ public class ComprehensiveClassTest { // 1. 嵌套 Enum public enum MyEnum { - RED, GREEN, BLUE + RED, + GREEN, + BLUE } // 2. 嵌套 Record diff --git a/test/fixtures/expected/ComprehensiveInterfaceTest.java b/test/fixtures/expected/ComprehensiveInterfaceTest.java index 4423998..710a169 100644 --- a/test/fixtures/expected/ComprehensiveInterfaceTest.java +++ b/test/fixtures/expected/ComprehensiveInterfaceTest.java @@ -7,9 +7,4 @@ public interface ComprehensiveInterfaceTest { // 2. 隐式默认 public 方法 void implicitDefaultMethod(); - - // 3. 显式 private 方法 - private void privateHelperMethod() { - // 私有方法 - } } diff --git a/test/fixtures/original/ComprehensiveClassTest.java b/test/fixtures/original/ComprehensiveClassTest.java index 70be842..cead947 100644 --- a/test/fixtures/original/ComprehensiveClassTest.java +++ b/test/fixtures/original/ComprehensiveClassTest.java @@ -33,7 +33,9 @@ public record NestedRecord(String name, int age) {} // 1. 嵌套 Enum public enum MyEnum { - RED, GREEN, BLUE + RED, + GREEN, + BLUE } // 11. 构造方法 B (双参数) diff --git a/test/fixtures/original/ComprehensiveInterfaceTest.java b/test/fixtures/original/ComprehensiveInterfaceTest.java index 4423998..12d3f0d 100644 --- a/test/fixtures/original/ComprehensiveInterfaceTest.java +++ b/test/fixtures/original/ComprehensiveInterfaceTest.java @@ -2,14 +2,9 @@ public interface ComprehensiveInterfaceTest { - // 1. 显式 public 方法 - public void explicitPublicMethod(); - // 2. 隐式默认 public 方法 void implicitDefaultMethod(); - // 3. 显式 private 方法 - private void privateHelperMethod() { - // 私有方法 - } + // 1. 显式 public 方法 + public void explicitPublicMethod(); }