Skip to content

Commit a70a343

Browse files
committed
fix(classpath): fix bug when generated sources are not considered by jdtls
1 parent 602a5f7 commit a70a343

4 files changed

Lines changed: 254 additions & 0 deletions

File tree

lua/java.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ function M.setup(custom_config)
7979
----------------------------------------------------------------------
8080
-- init --
8181
----------------------------------------------------------------------
82+
if config.experimental.fix_generated_sources then
83+
require('java.experimental.fix-generated-sources').patch(vim.fn.getcwd())
84+
end
85+
8286
require('java.startup.lsp_setup').setup(config)
8387
require('java.startup.decompile-watcher').setup()
8488
require('java-refactor').setup()

lua/java/config.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ local V = jdtls_version_map[JDTLS_VERSION]
2727
---@field java_debug_adapter { enable: boolean, version: string }
2828
---@field spring_boot_tools { enable: boolean, version: string }
2929
---@field jdk { auto_install: boolean, version: string }
30+
---@field experimental { fix_generated_sources: boolean }
3031
---@field log java-core.Log2Config
3132

3233
---@class java.PartialConfig
@@ -37,6 +38,7 @@ local V = jdtls_version_map[JDTLS_VERSION]
3738
---@field java_debug_adapter? { enable?: boolean, version?: string }
3839
---@field spring_boot_tools? { enable?: boolean, version?: string }
3940
---@field jdk? { auto_install?: boolean, version?: string }
41+
---@field experimental? { fix_generated_sources?: boolean }
4042
---@field log? java-core.PartialLog2Config
4143

4244
---@type java.Config
@@ -46,6 +48,10 @@ local config = {
4648
nvim_jdtls_conflict = true,
4749
},
4850

51+
experimental = {
52+
fix_generated_sources = false,
53+
},
54+
4955
jdtls = {
5056
version = JDTLS_VERSION,
5157
},
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
local path_utils = require('java-core.utils.path')
2+
3+
local M = {}
4+
5+
local function set_exclusions(line, exclusions)
6+
local exclusion_value = table.concat(exclusions, '|')
7+
if line:match('excluding="[^"]+"') then
8+
return line:gsub('excluding="([^"]+)"', 'excluding="' .. exclusion_value .. '"', 1)
9+
end
10+
11+
return line:gsub('<classpathentry ', '<classpathentry excluding="' .. exclusion_value .. '" ', 1)
12+
end
13+
14+
local function get_relative_path(root, absolute_path)
15+
local prefix = root .. path_utils.path_separator
16+
if absolute_path:sub(1, #prefix) == prefix then
17+
return absolute_path:sub(#prefix + 1)
18+
end
19+
20+
return absolute_path
21+
end
22+
23+
local function has_classpath_entry(lines, entry_path)
24+
local pattern = 'path="' .. vim.pesc(entry_path) .. '"'
25+
for _, line in ipairs(lines) do
26+
if line:find(pattern) then
27+
return true
28+
end
29+
end
30+
31+
return false
32+
end
33+
34+
local function find_generated_sources_entry(lines)
35+
for index, line in ipairs(lines) do
36+
if line:find('path="target/generated%-sources"') then
37+
return index
38+
end
39+
end
40+
41+
return nil
42+
end
43+
44+
local function add_classpath_entry(lines, entry_path)
45+
local entry = {
46+
'\t<classpathentry kind="src" output="target/classes" path="' .. entry_path .. '">',
47+
'\t\t<attributes>',
48+
'\t\t\t<attribute name="optional" value="true"/>',
49+
'\t\t\t<attribute name="maven.pomderived" value="true"/>',
50+
'\t\t</attributes>',
51+
'\t</classpathentry>',
52+
}
53+
54+
local output_index = #lines + 1
55+
for index, line in ipairs(lines) do
56+
if line:find('<classpathentry kind="output"') then
57+
output_index = index
58+
break
59+
end
60+
end
61+
62+
for offset = #entry, 1, -1 do
63+
table.insert(lines, output_index, entry[offset])
64+
end
65+
66+
return true
67+
end
68+
69+
local function get_generated_source_roots(module_root)
70+
local generated_root = path_utils.join(module_root, 'target', 'generated-sources')
71+
if not vim.uv.fs_stat(generated_root) then
72+
return {}
73+
end
74+
75+
local java_roots = vim.fs.find(function(name, generated_path)
76+
return name == 'java' and generated_path:find(vim.pesc(path_utils.join('target', 'generated-sources')), 1, false) ~= nil
77+
end, {
78+
path = generated_root,
79+
type = 'directory',
80+
limit = math.huge,
81+
})
82+
83+
local source_roots = {}
84+
local seen_roots = {}
85+
for _, java_root in ipairs(java_roots) do
86+
local relative_to_generated_root = get_relative_path(generated_root, java_root)
87+
local segments = vim.split(relative_to_generated_root, path_utils.path_separator, { plain = true })
88+
local segment_count = #segments
89+
90+
if segment_count >= 3 and segments[segment_count] == 'java' and segments[segment_count - 2] == 'src' then
91+
local source_root = get_relative_path(module_root, java_root)
92+
if not seen_roots[source_root] then
93+
seen_roots[source_root] = true
94+
table.insert(source_roots, source_root)
95+
end
96+
end
97+
end
98+
99+
table.sort(source_roots)
100+
return source_roots
101+
end
102+
103+
local function get_generated_source_exclusions(source_roots)
104+
local exclusions = { ['annotations/'] = true }
105+
local generated_root_prefix = path_utils.join('target', 'generated-sources') .. path_utils.path_separator
106+
107+
for _, source_root in ipairs(source_roots) do
108+
if source_root:sub(1, #generated_root_prefix) == generated_root_prefix then
109+
local relative_to_generated_root = source_root:sub(#generated_root_prefix + 1)
110+
local first_segment = vim.split(relative_to_generated_root, path_utils.path_separator, { plain = true })[1]
111+
if first_segment then
112+
exclusions[first_segment .. '/'] = true
113+
end
114+
end
115+
end
116+
117+
local ordered_exclusions = vim.tbl_keys(exclusions)
118+
table.sort(ordered_exclusions)
119+
return ordered_exclusions
120+
end
121+
122+
local function patch_module_classpath(classpath_file)
123+
local module_root = vim.fs.dirname(classpath_file)
124+
local lines = vim.fn.readfile(classpath_file)
125+
local generated_sources_entry_index = find_generated_sources_entry(lines)
126+
if not generated_sources_entry_index then
127+
return false
128+
end
129+
130+
local file_changed = false
131+
local source_roots = get_generated_source_roots(module_root)
132+
local line = lines[generated_sources_entry_index]
133+
local patched = set_exclusions(line, get_generated_source_exclusions(source_roots))
134+
135+
if patched ~= line then
136+
lines[generated_sources_entry_index] = patched
137+
file_changed = true
138+
end
139+
140+
for _, source_root in ipairs(source_roots) do
141+
if not has_classpath_entry(lines, source_root) then
142+
add_classpath_entry(lines, source_root)
143+
file_changed = true
144+
end
145+
end
146+
147+
if file_changed then
148+
vim.fn.writefile(lines, classpath_file)
149+
end
150+
151+
return file_changed
152+
end
153+
154+
function M.patch(root)
155+
local changed = false
156+
for _, file in ipairs(vim.fs.find('.classpath', { path = root, type = 'file', limit = math.huge })) do
157+
if patch_module_classpath(file) then
158+
changed = true
159+
end
160+
end
161+
162+
return changed
163+
end
164+
165+
return M
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
local assert = require('luassert')
2+
3+
describe('Generated sources classpath patcher', function()
4+
local classpath = require('java.experimental.fix-generated-sources')
5+
local path = require('java-core.utils.path')
6+
7+
local temp_dir
8+
local module_root
9+
local classpath_file
10+
11+
before_each(function()
12+
temp_dir = vim.fn.tempname()
13+
module_root = path.join(temp_dir, 'project')
14+
classpath_file = path.join(module_root, '.classpath')
15+
16+
vim.fn.mkdir(path.join(module_root, 'target', 'generated-sources', 'src', 'main', 'java'), 'p')
17+
vim.fn.mkdir(path.join(module_root, 'target', 'generated-sources', 'openapi', 'src', 'main', 'java'), 'p')
18+
vim.fn.mkdir(path.join(module_root, 'target', 'generated-sources', 'foo', 'main', 'java'), 'p')
19+
vim.fn.mkdir(path.join(module_root, 'target', 'generated-sources', 'openapi', 'main', 'java'), 'p')
20+
vim.fn.mkdir(path.join(module_root, 'target', 'generated-sources', 'java'), 'p')
21+
22+
vim.fn.writefile({
23+
'<?xml version="1.0" encoding="UTF-8"?>',
24+
'<classpath>',
25+
'\t<classpathentry kind="src" output="target/classes" path="src/main/java">',
26+
'\t\t<attributes>',
27+
'\t\t\t<attribute name="optional" value="true"/>',
28+
'\t\t\t<attribute name="maven.pomderived" value="true"/>',
29+
'\t\t</attributes>',
30+
'\t</classpathentry>',
31+
'\t<classpathentry kind="src" output="target/classes" path="target/generated-sources">',
32+
'\t\t<attributes>',
33+
'\t\t\t<attribute name="optional" value="true"/>',
34+
'\t\t\t<attribute name="maven.pomderived" value="true"/>',
35+
'\t\t</attributes>',
36+
'\t</classpathentry>',
37+
'\t<classpathentry kind="output" path="target/classes"/>',
38+
'</classpath>',
39+
}, classpath_file)
40+
end)
41+
42+
after_each(function()
43+
vim.fn.delete(temp_dir, 'rf')
44+
end)
45+
46+
it('adds only generated roots that end with src/{segment}/java and excludes them from the parent root', function()
47+
assert.is_true(classpath.patch(temp_dir))
48+
49+
local lines = vim.fn.readfile(classpath_file)
50+
local content = table.concat(lines, '\n')
51+
52+
assert.is_truthy(content:find('excluding="annotations/|openapi/|src/"', 1, true))
53+
assert.is_truthy(content:find('path="target/generated-sources/src/main/java"', 1, true))
54+
assert.is_truthy(content:find('path="target/generated-sources/openapi/src/main/java"', 1, true))
55+
assert.is_falsy(content:find('path="target/generated-sources/foo/main/java"', 1, true))
56+
assert.is_falsy(content:find('path="target/generated-sources/openapi/main/java"', 1, true))
57+
assert.is_falsy(content:find('path="target/generated-sources/java"', 1, true))
58+
end)
59+
60+
it('does not add generated roots when the parent generated-sources entry is missing', function()
61+
vim.fn.writefile({
62+
'<?xml version="1.0" encoding="UTF-8"?>',
63+
'<classpath>',
64+
'\t<classpathentry kind="src" output="target/classes" path="src/main/java">',
65+
'\t\t<attributes>',
66+
'\t\t\t<attribute name="optional" value="true"/>',
67+
'\t\t\t<attribute name="maven.pomderived" value="true"/>',
68+
'\t\t</attributes>',
69+
'\t</classpathentry>',
70+
'\t<classpathentry kind="output" path="target/classes"/>',
71+
'</classpath>',
72+
}, classpath_file)
73+
74+
assert.is_false(classpath.patch(temp_dir))
75+
76+
local content = table.concat(vim.fn.readfile(classpath_file), '\n')
77+
assert.is_falsy(content:find('target/generated-sources/src/main/java', 1, true))
78+
end)
79+
end)

0 commit comments

Comments
 (0)