Skip to content

Commit a34c811

Browse files
committed
Adds utility for checking import order in espresso.
1 parent 0c2731c commit a34c811

File tree

3 files changed

+238
-2
lines changed

3 files changed

+238
-2
lines changed

espresso/ci/ci_common/common.jsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ local benchmark_suites = ['dacapo', 'renaissance', 'scala-dacapo'];
277277

278278
local _builds = [
279279
// Gates
280-
that.jdk21_gate_linux_amd64 + that.eclipse + that.jdt + that.predicates(false, false, false) + that.espresso_gate(allow_warnings=false, tags='style,fullbuild', timelimit='35:00', name='gate-espresso-style-jdk21-linux-amd64'),
280+
that.jdk21_gate_linux_amd64 + that.eclipse + that.jdt + that.predicates(false, false, false) + that.espresso_gate(allow_warnings=false, tags='style,fullbuild,imports', timelimit='35:00', name='gate-espresso-style-jdk21-linux-amd64'),
281281
],
282282

283283
builds: utils.add_defined_in(_builds, std.thisFile),
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#
2+
# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
#
5+
# This code is free software; you can redistribute it and/or modify it
6+
# under the terms of the GNU General Public License version 2 only, as
7+
# published by the Free Software Foundation.
8+
#
9+
# This code is distributed in the hope that it will be useful, but WITHOUT
10+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
# version 2 for more details (a copy is included in the LICENSE file that
13+
# accompanied this code).
14+
#
15+
# You should have received a copy of the GNU General Public License version
16+
# 2 along with this work; if not, write to the Free Software Foundation,
17+
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
#
19+
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
# or visit www.oracle.com if you need additional information or have any
21+
# questions.
22+
#
23+
24+
"""Provides import ordering capabilities for .java files."""
25+
26+
from glob import iglob
27+
28+
# If a given line is an import, it will end with this suffix.
29+
# Used to strip this suffix for faster string comparison.
30+
SUFFIX = ";\n"
31+
32+
STATIC_PREFIX = "import static "
33+
REGULAR_PREFIX = "import "
34+
35+
def verify_order(path, prefix_order):
36+
"""
37+
Verifies import order of java files under the given path.
38+
39+
Iterates over all '.java' files in the given path, recursively over its subfolders.
40+
It then checks that imports in these files are ordered.
41+
42+
Here are the rules:
43+
1. All imports that starts with a suffix that appears in this list must
44+
be found before any other import with a suffix that appears later in
45+
this list.
46+
2. All imports with a given suffix must be in lexicographic order within
47+
all other imports with the same prefix.
48+
3. Static imports must appear before regular imports.
49+
50+
:param path: Where to look for the java files.
51+
:param prefix_order: An ordered list of expected suffixes.
52+
:return: The list of files violating the specified order.
53+
"""
54+
55+
# Validate the prefixes
56+
err = validate_format(prefix_order)
57+
if err:
58+
# Failure is represented with a non-empty list
59+
return [err]
60+
61+
# Start building definitive list of import prefixes
62+
static_prefixes = []
63+
regular_prefixes = []
64+
65+
for prefix in prefix_order:
66+
if prefix:
67+
# If prefix is "abc", add "import static abc"
68+
static_prefixes.append(STATIC_PREFIX + prefix + '.')
69+
# If prefix is "abc", add "import abc"
70+
regular_prefixes.append(REGULAR_PREFIX + prefix + '.')
71+
else:
72+
# Empty prefix means everything will match.
73+
# Empty prefix is added manually below.
74+
break
75+
76+
# Ensure we have the empty prefix
77+
# Add "import static "
78+
static_prefixes.append(STATIC_PREFIX)
79+
# Add "import "
80+
regular_prefixes.append(REGULAR_PREFIX)
81+
82+
# Ensures static imports are before regular imports.
83+
prefix_format = static_prefixes + regular_prefixes
84+
85+
invalid_files = []
86+
87+
def is_sorted(li):
88+
if len(li) <= 1:
89+
return True
90+
return all(li[i] <= li[i + 1] for i in range(len(li) - 1))
91+
92+
def check_file(to_check, prefix_format):
93+
imports, prefix_ordered = get_imports(to_check, prefix_format)
94+
95+
if not prefix_ordered:
96+
return False
97+
98+
for import_list in imports:
99+
if not is_sorted(import_list):
100+
return False
101+
102+
return True
103+
104+
for file in iglob(path + '/**/*.java', recursive=True):
105+
if not check_file(file, prefix_format):
106+
invalid_files.append(file)
107+
108+
return invalid_files
109+
110+
def validate_format(prefix_order):
111+
"""
112+
Validates a given ordered list of prefix for import order verification.
113+
114+
Returns the reason for failure of validation if any, or an empty string
115+
if the prefixes are well-formed.
116+
"""
117+
for prefix in prefix_order:
118+
if prefix.endswith('.'):
119+
return "Invalid format for the ordered prefixes: \n'" + prefix + "' must not end with a '.'"
120+
return ""
121+
122+
def get_imports(file, prefix_format):
123+
"""
124+
Obtains list of imports list, each corresponding to each specified prefix.
125+
Also returns whether the found prefixes were ordered.
126+
127+
In case the prefixes where not ordered, the last element of the returned list will contain
128+
every import after the violating line
129+
"""
130+
def add_import(li, value, prefix, suf):
131+
to_add = value[len(prefix):]
132+
if to_add.endswith(suf):
133+
to_add = to_add[:len(to_add) - len(suf)]
134+
li.append(to_add)
135+
136+
def enter_fail_state(imports, prefix_format, cur_prefix_imports):
137+
if cur_prefix_imports:
138+
imports.append(cur_prefix_imports)
139+
return False, len(prefix_format), ""
140+
141+
with open(file) as f:
142+
imports = []
143+
prefix_ordered = True
144+
145+
cur_prefix_idx = 0
146+
cur_prefix = prefix_format[cur_prefix_idx]
147+
148+
cur_prefix_imports = []
149+
150+
for line in f.readlines():
151+
ignore = not line.startswith("import")
152+
if ignore:
153+
# start of class declaration, we can stop looking for imports.
154+
end = 'class ' in line or 'interface ' in line or 'enum ' in line or 'record ' in line
155+
if end:
156+
break
157+
continue
158+
159+
if line.startswith(cur_prefix):
160+
# If we are still ensuring prefix ordering, ensure that this line does not belong
161+
# to a previous prefix.
162+
if prefix_ordered:
163+
for i in range(cur_prefix_idx):
164+
if line.startswith(prefix_format[i]):
165+
# A match for a previous prefix was found: enter fail state
166+
prefix_ordered, cur_prefix_idx, cur_prefix = enter_fail_state(imports, prefix_format, cur_prefix_imports)
167+
cur_prefix_imports = []
168+
add_import(cur_prefix_imports, line, cur_prefix, SUFFIX)
169+
else:
170+
# cur_prefix not found, advance to next prefix if found, report failure if not.
171+
for i in range(cur_prefix_idx + 1, len(prefix_format)):
172+
if line.startswith(prefix_format[i]):
173+
# Report imports for current prefix,
174+
if cur_prefix_imports:
175+
imports.append(cur_prefix_imports)
176+
# Set state to next prefix.
177+
cur_prefix = prefix_format[i]
178+
cur_prefix_idx = i
179+
cur_prefix_imports = []
180+
add_import(cur_prefix_imports, line, cur_prefix, SUFFIX)
181+
break
182+
else:
183+
# On failure, dump remaining lines into the last cur_prefix_imports.
184+
prefix_ordered, cur_prefix_idx, cur_prefix = enter_fail_state(imports, prefix_format, cur_prefix_imports)
185+
cur_prefix_imports = []
186+
add_import(cur_prefix_imports, line, cur_prefix, SUFFIX)
187+
188+
if cur_prefix_imports:
189+
imports.append(cur_prefix_imports)
190+
191+
return imports, prefix_ordered

espresso/mx.espresso/mx_espresso.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@
4141
from mx_gate import Task, add_gate_runner
4242
from mx_jackpot import jackpot
4343
from os.path import join, isabs, exists, dirname, relpath, basename
44+
from import_order import verify_order, validate_format
4445

4546
_suite = mx.suite('espresso')
4647

4748
# re-export custom mx project classes, so they can be used from suite.py
48-
from mx_sdk_shaded import ShadedLibraryProject # pylint: disable=unused-import
49+
from mx_sdk_shaded import ShadedLibraryProject
4950

5051
# JDK compiled with the Sulong toolchain.
5152
espresso_llvm_java_home = mx.get_env('ESPRESSO_LLVM_JAVA_HOME') or mx.get_env('LLVM_JAVA_HOME')
@@ -134,9 +135,48 @@ def _run_espresso_meta(args, nonZeroIsFatal=True, timeout=None):
134135
] + _espresso_standalone_command(args, allow_jacoco=False), nonZeroIsFatal=nonZeroIsFatal, timeout=timeout)
135136

136137

138+
def _run_verify_imports(s):
139+
# Look for the format specification in the suite
140+
prefs = s.eclipse_settings_sources().get('org.eclipse.jdt.ui.prefs')
141+
prefix_order = []
142+
if prefs:
143+
for pref in prefs:
144+
with open(pref) as f:
145+
for line in f.readlines():
146+
if line.startswith('org.eclipse.jdt.ui.importorder'):
147+
key_value_sep_index = line.find('=')
148+
if key_value_sep_index != -1:
149+
value = line.strip()[key_value_sep_index + 1:]
150+
prefix_order = value.split(';')
151+
152+
# Validate import order format
153+
err = validate_format(prefix_order)
154+
if err:
155+
mx.abort(err)
156+
157+
# Find invalid files
158+
invalid_files = []
159+
for project in s.projects:
160+
if isinstance(project, ShadedLibraryProject):
161+
# Ignore shaded libraries
162+
continue
163+
for src_dir in project.source_dirs():
164+
invalid_files += verify_order(src_dir, prefix_order)
165+
166+
if invalid_files:
167+
mx.abort("The following files have wrong imports order:\n" + '\n'.join(invalid_files))
168+
169+
print("All imports correctly ordered!")
170+
171+
def _run_verify_imports_espresso(args):
172+
if args:
173+
mx.abort("No arguments expected for verify-imports")
174+
_run_verify_imports(_suite)
175+
137176
class EspressoTags:
138177
jackpot = 'jackpot'
139178
verify = 'verify'
179+
imports = 'imports'
140180

141181

142182
def _espresso_gate_runner(args, tasks):
@@ -149,6 +189,10 @@ def _espresso_gate_runner(args, tasks):
149189
if t:
150190
mx_sdk_vm.verify_graalvm_configs(suites=['espresso'])
151191

192+
with Task('Espresso: verify import order', tasks, tags=[EspressoTags.imports]) as t:
193+
if t:
194+
_run_verify_imports(_suite)
195+
152196
mokapot_header_gate_name = 'Verify consistency of mokapot headers'
153197
with Task(mokapot_header_gate_name, tasks, tags=[EspressoTags.verify]) as t:
154198
if t:
@@ -831,6 +875,7 @@ def gen_gc_option_check(args):
831875
'java-truffle': [_run_java_truffle, '[args]'],
832876
'espresso-meta': [_run_espresso_meta, '[args]'],
833877
'gen-gc-option-check': [gen_gc_option_check, '[path to isolate-creation-only-options.txt]'],
878+
'verify-imports': [_run_verify_imports_espresso, ''],
834879
})
835880

836881

0 commit comments

Comments
 (0)