Skip to content

Commit 779a711

Browse files
puremourningbstaletic
authored andcommitted
Java: Fix gradle project root detection
Typical "modern" layout from `gradle init`: - settings.gradle - app/build.gradle - ... If you open app/src/java/.../Foo.java, we incorrectly pick app/ as the project root. Now, when searching for the project root, we continue to search the varioius file names, but we tag each file name with the "kind" and keep searching for other files of the same *kind* rather than just the same name. Meanwhile, we prefer finding the gradle/maven project. The reason for this is that jdt.ls spams .project files all over the codebase which makes any project you've previously opened look like an eclipse project. This is fine if the project root was previously correctly determined and/or the projects properly imported, but means that we never self-correct. So prefer finding the non-native project types so that we pick the correct root in _most_ cases.
1 parent 5de7052 commit 779a711

File tree

26 files changed

+822
-11
lines changed

26 files changed

+822
-11
lines changed

ycmd/completers/java/java_completer.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import shutil
2323
import tempfile
2424
import threading
25+
from collections import OrderedDict
2526

2627
from ycmd import responses, utils
2728
from ycmd.completers.language_server import language_server_completer
@@ -41,12 +42,13 @@
4142

4243
PATH_TO_JAVA = None
4344

44-
PROJECT_FILE_TAILS = [
45-
'.project',
46-
'pom.xml',
47-
'build.gradle',
48-
'build.gradle.kts'
49-
]
45+
PROJECT_FILE_TAILS = OrderedDict( {
46+
'pom.xml': 'maven',
47+
'build.gradle': 'gradle',
48+
'build.gradle.kts': 'gradle',
49+
'settings.gradle': 'gradle',
50+
'.project': 'eclipse',
51+
} )
5052

5153
DEFAULT_WORKSPACE_ROOT_PATH = os.path.abspath( os.path.join(
5254
os.path.dirname( __file__ ),
@@ -230,19 +232,19 @@ def _LauncherConfiguration( user_options, workspace_root, wipe_config ):
230232

231233

232234
def _MakeProjectFilesForPath( path ):
233-
for tail in PROJECT_FILE_TAILS:
234-
yield os.path.join( path, tail ), tail
235+
for tail, type in PROJECT_FILE_TAILS.items():
236+
yield os.path.join( path, tail ), tail, type
235237

236238

237239
def _FindProjectDir( starting_dir ):
238240
project_path = starting_dir
239241
project_type = None
240242

241243
for folder in utils.PathsToAllParentFolders( starting_dir ):
242-
for project_file, tail in _MakeProjectFilesForPath( folder ):
244+
for project_file, tail, type in _MakeProjectFilesForPath( folder ):
243245
if os.path.isfile( project_file ):
244246
project_path = folder
245-
project_type = tail
247+
project_type = type
246248
break
247249
if project_type:
248250
break
@@ -254,9 +256,14 @@ def _FindProjectDir( starting_dir ):
254256
LOGGER.debug( 'Found %s style project in %s. Searching for '
255257
'project root:', project_type, project_path )
256258

259+
file_types_to_search_for = [
260+
tail for tail, type in PROJECT_FILE_TAILS.items() if type == project_type
261+
]
262+
257263
for folder in utils.PathsToAllParentFolders( os.path.join( project_path,
258264
'..' ) ):
259-
if os.path.isfile( os.path.join( folder, project_type ) ):
265+
if any( os.path.isfile( os.path.join( folder, tail ) )
266+
for tail in file_types_to_search_for ):
260267
LOGGER.debug( ' %s is a parent project dir', folder )
261268
project_path = folder
262269
else:

ycmd/tests/java/server_management_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@
3939
StartJavaCompleterServerWithFile )
4040
from ycmd.tests.test_utils import ( BuildRequest,
4141
CompleterProjectDirectoryMatcher,
42+
LocationMatcher,
4243
MockProcessTerminationTimingOut,
44+
RangeMatcher,
4345
TemporaryTestDir,
46+
WaitForDiagnosticsToBeReady,
4447
WaitUntilCompleterServerReady )
4548
from ycmd import utils, handlers
4649

@@ -296,6 +299,45 @@ def test_ServerManagement_ProjectDetection_MavenParent_Submodule( self, app ):
296299
CompleterProjectDirectoryMatcher( project ) )
297300

298301

302+
@TidyJDTProjectFiles( PathToTestFile( 'gradle-init' ) )
303+
@IsolatedYcmd()
304+
def test_ServerManagement_ProjectDetection_GradleMultipleGradleFiles( self,
305+
app ):
306+
testfile = PathToTestFile( 'gradle-init',
307+
'app',
308+
'src',
309+
'main',
310+
'java',
311+
'org',
312+
'example',
313+
'app',
314+
'App.java' )
315+
project = PathToTestFile( 'gradle-init' )
316+
317+
StartJavaCompleterServerWithFile( app, testfile )
318+
319+
# Run the debug info to check that we have the correct project dir
320+
request_data = BuildRequest( filetype = 'java' )
321+
assert_that( app.post_json( '/debug_info', request_data ).json,
322+
CompleterProjectDirectoryMatcher( project ) )
323+
324+
# Check that we successfully actually parse the project too
325+
contents = utils.ReadFile( testfile )
326+
diags = WaitForDiagnosticsToBeReady( app, testfile, contents, 'java' )
327+
assert_that( diags, has_item(
328+
has_entries( {
329+
'kind': 'WARNING',
330+
'text': 'The value of the local variable unused is not used '
331+
'[536870973]',
332+
'location': LocationMatcher( testfile, 16, 16 ),
333+
'location_extent': RangeMatcher( testfile, ( 16, 16 ), ( 16, 22 ) ),
334+
'ranges': contains_exactly(
335+
RangeMatcher( testfile, ( 16, 16 ), ( 16, 22 ) ) ),
336+
'fixit_available': False
337+
} ),
338+
) )
339+
340+
299341
def test_ServerManagement_ProjectDetection_NoParent( self ):
300342
with TemporaryTestDir() as tmp_dir:
301343
with isolated_app() as app:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#
2+
# https://help.github.com/articles/dealing-with-line-endings/
3+
#
4+
# Linux start script should use lf
5+
/gradlew text eol=lf
6+
7+
# These are Windows script files and should use crlf
8+
*.bat text eol=crlf
9+
10+
# Binary files should be left untouched
11+
*.jar binary
12+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Ignore Gradle project-specific cache directory
2+
.gradle
3+
4+
# Ignore Gradle build output directory
5+
build
6+
.project
7+
.classpath
8+
.settings/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* This file was generated by the Gradle 'init' task.
3+
*/
4+
5+
plugins {
6+
id 'buildlogic.java-application-conventions'
7+
}
8+
9+
dependencies {
10+
implementation 'org.apache.commons:commons-text'
11+
implementation project(':utilities')
12+
}
13+
14+
application {
15+
// Define the main class for the application.
16+
mainClass = 'org.example.app.App'
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This source file was generated by the Gradle 'init' task
3+
*/
4+
package org.example.app;
5+
6+
import org.example.list.LinkedList;
7+
8+
import static org.example.utilities.StringUtils.join;
9+
import static org.example.utilities.StringUtils.split;
10+
import static org.example.app.MessageUtils.getMessage;
11+
12+
import org.apache.commons.text.WordUtils;
13+
14+
public class App {
15+
public static void main(String[] args) {
16+
String unused;
17+
LinkedList tokens;
18+
tokens = split(getMessage());
19+
String result = join(tokens);
20+
System.out.println(WordUtils.capitalize(result));
21+
}
22+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* This source file was generated by the Gradle 'init' task
3+
*/
4+
package org.example.app;
5+
6+
class MessageUtils {
7+
public static String getMessage() {
8+
return "Hello World!";
9+
}
10+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* This source file was generated by the Gradle 'init' task
3+
*/
4+
package org.example.app;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class MessageUtilsTest {
11+
@Test void testGetMessage() {
12+
assertEquals("Hello World!", MessageUtils.getMessage());
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* This file was generated by the Gradle 'init' task.
3+
*/
4+
5+
plugins {
6+
// Support convention plugins written in Groovy. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build.
7+
id 'groovy-gradle-plugin'
8+
}
9+
10+
repositories {
11+
// Use the plugin portal to apply community plugins in convention plugins.
12+
gradlePluginPortal()
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* This file was generated by the Gradle 'init' task.
3+
*
4+
* This settings file is used to specify which projects to include in your build-logic build.
5+
*/
6+
7+
dependencyResolutionManagement {
8+
// Reuse version catalog from the main build.
9+
versionCatalogs {
10+
create('libs', { from(files("../gradle/libs.versions.toml")) })
11+
}
12+
}
13+
14+
rootProject.name = 'buildSrc'

0 commit comments

Comments
 (0)