Skip to content

Commit 91a723a

Browse files
vkuceraalibuild
andauthored
Improve O2 linter (#10097)
Co-authored-by: ALICE Builder <alibuild@users.noreply.github.com>
1 parent 71eaa7b commit 91a723a

File tree

1 file changed

+48
-28
lines changed

1 file changed

+48
-28
lines changed

Scripts/o2_linter.py

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class TestSpec(ABC):
163163
suffixes: "list[str]" = [] # suffixes of files to test
164164
per_line = True # Test lines separately one by one.
165165
n_issues = 0 # issue counter
166+
n_disabled = 0 # counter of disabled issues
166167

167168
def file_matches(self, path: str) -> bool:
168169
"""Test whether the path matches the pattern for files to test."""
@@ -175,7 +176,10 @@ def is_disabled(self, line: str, prefix_comment="//") -> bool:
175176
return False
176177
line = line[(line.index(prefix) + len(prefix)) :] # Strip away part before prefix.
177178
if self.name in line:
178-
return True
179+
self.n_disabled += 1
180+
# Look for a comment with a reason for disabling.
181+
if re.search(r" \([\w\s]{3,}\)", line):
182+
return True
179183
return False
180184

181185
def test_line(self, line: str) -> bool:
@@ -225,7 +229,7 @@ class TestIOStream(TestSpec):
225229
"""Detect included iostream."""
226230

227231
name = "include-iostream"
228-
message = "Including iostream is discouraged. Use O2 logging instead."
232+
message = "Do not include iostream. Use O2 logging instead."
229233
suffixes = [".h", ".cxx"]
230234

231235
def test_line(self, line: str) -> bool:
@@ -238,7 +242,7 @@ class TestUsingStd(TestSpec):
238242
"""Detect importing names from the std namespace."""
239243

240244
name = "import-std-name"
241-
message = "Importing names from the std namespace is not allowed in headers."
245+
message = "Do not import names from the std namespace in headers."
242246
suffixes = [".h"]
243247

244248
def test_line(self, line: str) -> bool:
@@ -251,7 +255,7 @@ class TestUsingDirectives(TestSpec):
251255
"""Detect using directives in headers."""
252256

253257
name = "using-directive"
254-
message = "Using directives are not allowed in headers."
258+
message = "Do not put using directives at global scope in headers."
255259
suffixes = [".h"]
256260

257261
def test_line(self, line: str) -> bool:
@@ -308,7 +312,7 @@ class TestROOT(TestSpec):
308312
"""Detect unnecessary use of ROOT entities."""
309313

310314
name = "root-entity"
311-
message = "Consider replacing ROOT entities with equivalents from standard C++ or from O2."
315+
message = "Replace ROOT entities with equivalents from standard C++ or from O2."
312316
suffixes = [".h", ".cxx"]
313317

314318
def file_matches(self, path: str) -> bool:
@@ -329,14 +333,14 @@ class TestPi(TestSpec):
329333
"""Detect use of external pi."""
330334

331335
name = "external-pi"
332-
message = "Consider using the PI constant (and its multiples and fractions) defined in o2::constants::math."
336+
message = "Use the PI constant (and its multiples and fractions) defined in o2::constants::math."
333337
suffixes = [".h", ".cxx"]
334338

335339
def file_matches(self, path: str) -> bool:
336340
return super().file_matches(path) and "Macros/" not in path
337341

338342
def test_line(self, line: str) -> bool:
339-
pattern = r"M_PI|TMath::(Two)?Pi"
343+
pattern = r"[^\w]M_PI|TMath::(Two)?Pi"
340344
if is_comment_cpp(line):
341345
return True
342346
line = remove_comment_cpp(line)
@@ -347,7 +351,7 @@ class TestTwoPiAddSubtract(TestSpec):
347351
"""Detect adding/subtracting of 2 pi."""
348352

349353
name = "two-pi-add-subtract"
350-
message = "Consider using RecoDecay::constrainAngle to restrict angle to a given range."
354+
message = "Use RecoDecay::constrainAngle to restrict angle to a given range."
351355
suffixes = [".h", ".cxx"]
352356

353357
def test_line(self, line: str) -> bool:
@@ -366,14 +370,14 @@ class TestPiMultipleFraction(TestSpec):
366370
"""Detect multiples/fractions of pi for existing equivalent constants."""
367371

368372
name = "pi-multiple-fraction"
369-
message = "Consider using multiples/fractions of PI defined in o2::constants::math."
373+
message = "Use multiples/fractions of PI defined in o2::constants::math."
370374
suffixes = [".h", ".cxx"]
371375

372376
def test_line(self, line: str) -> bool:
373377
pattern_pi = r"(M_PI|TMath::(Two)?Pi\(\)|(((o2::)?constants::)?math::)?(Two)?PI)"
374378
pattern_multiple = r"(2(\.0*f?)?|0\.2?5f?) \* " # * 2, 0.25, 0.5
375379
pattern_fraction = r" / ((2|3|4)([ ,;\)]|\.0*f?))" # / 2, 3, 4
376-
pattern = rf"{pattern_multiple}{pattern_pi}|{pattern_pi}{pattern_fraction}"
380+
pattern = rf"{pattern_multiple}{pattern_pi}[^\w]|{pattern_pi}{pattern_fraction}"
377381
if is_comment_cpp(line):
378382
return True
379383
line = remove_comment_cpp(line)
@@ -385,8 +389,8 @@ class TestPdgDatabase(TestSpec):
385389

386390
name = "pdg/database"
387391
message = (
388-
"Direct use of TDatabasePDG is not allowed. "
389-
"Use o2::constants::physics::Mass... or Service<o2::framework::O2DatabasePDG>."
392+
"Do not use TDatabasePDG directly. "
393+
"Use o2::constants::physics::Mass... or Service<o2::framework::O2DatabasePDG> instead."
390394
)
391395
suffixes = [".h", ".cxx"]
392396

@@ -413,7 +417,7 @@ def test_line(self, line: str) -> bool:
413417
line = remove_comment_cpp(line)
414418
if re.search(r"->(GetParticle|Mass)\([+-]?[0-9]+\)", line):
415419
return False
416-
match = re.search(r"[Pp][Dd][Gg][\w]* ={1,2} [+-]?([0-9]+);", line)
420+
match = re.search(r"[Pp][Dd][Gg].* !?={1,2} [+-]?([0-9]+)", line)
417421
if match:
418422
code = match.group(1)
419423
if code not in ("0", "1", "999"):
@@ -425,9 +429,7 @@ class TestPdgMass(TestSpec):
425429
"""Detect unnecessary call of Mass() for a known PDG code."""
426430

427431
name = "pdg/known-mass"
428-
message = (
429-
"Consider using o2::constants::physics::Mass... instead of calling a database method for a known PDG code."
430-
)
432+
message = "Use o2::constants::physics::Mass... instead of calling a database method for a known PDG code."
431433
suffixes = [".h", ".cxx", ".C"]
432434

433435
def test_line(self, line: str) -> bool:
@@ -446,7 +448,7 @@ class TestLogging(TestSpec):
446448
"""Detect non-O2 logging."""
447449

448450
name = "logging"
449-
message = "Consider using O2 logging (LOG, LOGF, LOGP)."
451+
message = "Use O2 logging (LOG, LOGF, LOGP)."
450452
suffixes = [".h", ".cxx"]
451453

452454
def file_matches(self, path: str) -> bool:
@@ -694,6 +696,8 @@ def test_line(self, line: str) -> bool:
694696
"namespace",
695697
"struct",
696698
"class",
699+
"explicit",
700+
"concept",
697701
):
698702
return True
699703
if len(words) > 2 and words[1] in ("typename", "class", "struct"):
@@ -792,6 +796,8 @@ def test_line(self, line: str) -> bool:
792796
constant_name = constant_name[: constant_name.index("[")]
793797
if "::" in constant_name: # Remove the class prefix for methods.
794798
constant_name = constant_name.split("::")[-1]
799+
if "#" in constant_name: # Remove "#" for strings in macros.
800+
constant_name = constant_name[: constant_name.index("#")]
795801
# The actual test comes here.
796802
if constant_name.startswith("k") and len(constant_name) > 1: # exception for special constants
797803
constant_name = constant_name[1:] # test the name without "k"
@@ -822,6 +828,10 @@ def test_line(self, line: str) -> bool:
822828
# return True
823829
if column_type_name[0] == "_": # probably a macro variable
824830
return True
831+
if "#" in column_type_name: # Remove "#" for strings in macros.
832+
column_type_name = column_type_name[: column_type_name.index("#")]
833+
if "#" in column_getter_name: # Remove "#" for strings in macros.
834+
column_getter_name = column_getter_name[: column_getter_name.index("#")]
825835
# The actual test comes here.
826836
if not is_upper_camel_case(column_type_name):
827837
return False
@@ -858,6 +868,8 @@ def test_line(self, line: str) -> bool:
858868
# print(f"Got versioned table \"{table_type_name}\", version {table_version}")
859869
if table_type_name[0] == "_": # probably a macro variable
860870
return True
871+
if "#" in table_type_name: # Remove "#" for strings in macros.
872+
table_type_name = table_type_name[: table_type_name.index("#")]
861873
# The actual test comes here.
862874
return is_upper_camel_case(table_type_name)
863875

@@ -888,16 +900,16 @@ class TestNameType(TestSpec):
888900
"""Test names of defined types."""
889901

890902
name = "name/type"
891-
message = "Use UpperCamelCase for names of defined types."
903+
message = "Use UpperCamelCase for names of defined types (including concepts)."
892904
suffixes = [".h", ".cxx", ".C"]
893905

894906
def test_line(self, line: str) -> bool:
895907
if is_comment_cpp(line):
896908
return True
897-
if not (match := re.match(r"using (\w+) = ", line)):
909+
if not (match := re.match(r"(using|concept) (\w+) = ", line)):
898910
return True
899911
# Extract type name.
900-
type_name = match.group(1)
912+
type_name = match.group(2)
901913
# The actual test comes here.
902914
return is_upper_camel_case(type_name)
903915

@@ -1193,8 +1205,13 @@ def file_matches(self, path: str) -> bool:
11931205
def test_file(self, path: str, content) -> bool:
11941206
file_name = os.path.basename(path).rstrip(".cxx")
11951207
base_struct_name = f"{file_name[0].upper()}{file_name[1:]}" # expected base of struct names
1196-
if "PWGHF/" in path:
1197-
base_struct_name = "Hf" + base_struct_name
1208+
if match := re.search("PWG([A-Z]{2})/", path):
1209+
name_pwg = match.group(1)
1210+
prefix_pwg = name_pwg.capitalize()
1211+
if name_pwg in ("HF"):
1212+
base_struct_name = rf"{prefix_pwg}{base_struct_name}" # mandatory PWG prefix
1213+
else:
1214+
base_struct_name = rf"({prefix_pwg})?{base_struct_name}" # optional PWG prefix
11981215
# print(f"For file {file_name} expecting to find {base_struct_name}.")
11991216
struct_names = [] # actual struct names in the file
12001217
for line in content:
@@ -1204,13 +1221,13 @@ def test_file(self, path: str, content) -> bool:
12041221
continue
12051222
# Extract struct name.
12061223
words = line.split()
1207-
if not words[1].isalnum(): # "struct : ..."
1224+
if not words[1].isidentifier(): # "struct : ..."
12081225
continue
12091226
struct_name = words[1]
12101227
struct_names.append(struct_name)
12111228
# print(f"Found structs: {struct_names}.")
12121229
for struct_name in struct_names:
1213-
if struct_name.startswith(base_struct_name):
1230+
if re.match(base_struct_name, struct_name):
12141231
return True
12151232
return False
12161233

@@ -1486,9 +1503,12 @@ def main():
14861503
# Report results per test.
14871504
print("\nResults per test")
14881505
len_max = max(len(name) for name in test_names)
1489-
print(f"test{' ' * (len_max - len('test'))}\tissues\tbad files")
1506+
print(f"test{' ' * (len_max - len('test'))}\tissues\tdisabled\tbad files")
14901507
for test in tests:
1491-
print(f"{test.name}{' ' * (len_max - len(test.name))}\t{test.n_issues}\t{n_files_bad[test.name]}")
1508+
print(
1509+
f"{test.name}{' ' * (len_max - len(test.name))}\t{test.n_issues}\t{test.n_disabled}"
1510+
f"\t\t{n_files_bad[test.name]}"
1511+
)
14921512

14931513
# Report global result.
14941514
title_result = "O2 linter result"
@@ -1501,8 +1521,8 @@ def main():
15011521
else:
15021522
msg_result = "Issues have been found."
15031523
msg_disable = (
1504-
f'You can disable a test for a line by adding a comment with "{prefix_disable}"'
1505-
" followed by the name of the test."
1524+
f'Exceptionally, you can disable a test for a line by adding a comment with "{prefix_disable}"'
1525+
" followed by the name of the test and parentheses with a reason for the exception."
15061526
)
15071527
if github_mode:
15081528
print(f"::error title={title_result}::{msg_result}")

0 commit comments

Comments
 (0)