Skip to content

Commit eb0f175

Browse files
authored
O2 linter: Improve search patterns (#11398)
1 parent 53564cf commit eb0f175

File tree

1 file changed

+68
-85
lines changed

1 file changed

+68
-85
lines changed

Scripts/o2_linter.py

Lines changed: 68 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -47,33 +47,39 @@ class Severity(Enum):
4747

4848

4949
class Reference(Enum):
50-
O2 = 1
51-
ISO_CPP = 2
52-
LLVM = 3
53-
GOOGLE = 4
54-
LINTER = 5
55-
PWG_HF = 6
56-
PY_ZEN = 7
57-
PY_PEP8 = 8
50+
ISO_CPP = 1
51+
LLVM = 2
52+
GOOGLE = 3
53+
PY_ZEN = 4
54+
PY_PEP8 = 5
55+
O2 = 6
56+
PWG_HF = 7
57+
LINTER_1 = 8
58+
LINTER_2 = 9
5859

5960

6061
references_list: "list[tuple[Reference, str, str]]" = [
61-
(Reference.O2, "ALICE O2 Coding Guidelines", "https://github.com/AliceO2Group/CodingGuidelines"),
6262
(Reference.ISO_CPP, "C++ Core Guidelines", "https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines"),
6363
(Reference.LLVM, "LLVM Coding Standards", "https://llvm.org/docs/CodingStandards.html"),
6464
(Reference.GOOGLE, "Google C++ Style Guide", "https://google.github.io/styleguide/cppguide.html"),
65+
(Reference.PY_ZEN, "The Zen of Python", "https://peps.python.org/pep-0020/"),
66+
(Reference.PY_PEP8, "Style Guide for Python Code", "https://peps.python.org/pep-0008/"),
67+
(Reference.O2, "ALICE O2 Coding Guidelines", "https://github.com/AliceO2Group/CodingGuidelines"),
68+
(
69+
Reference.PWG_HF,
70+
"PWG-HF guidelines",
71+
"https://aliceo2group.github.io/analysis-framework/docs/advanced-specifics/pwghf.html#contribute",
72+
),
6573
(
66-
Reference.LINTER,
74+
Reference.LINTER_1,
6775
"Proposal of the O2 linter",
6876
"https://indico.cern.ch/event/1482467/#29-development-of-the-o2-linte",
6977
),
7078
(
71-
Reference.PWG_HF,
72-
"PWG-HF guidelines",
73-
"https://aliceo2group.github.io/analysis-framework/docs/advanced-specifics/pwghf.html#contribute",
79+
Reference.LINTER_2,
80+
"Update of the O2 linter",
81+
"https://indico.cern.ch/event/1513748/#29-o2-linter-development",
7482
),
75-
(Reference.PY_ZEN, "The Zen of Python", "https://peps.python.org/pep-0020/"),
76-
(Reference.PY_PEP8, "Style Guide for Python Code", "https://peps.python.org/pep-0008/"),
7783
]
7884

7985
references: "dict[Reference, dict]" = {name: {"title": title, "url": url} for name, title, url in references_list}
@@ -312,7 +318,7 @@ class TestIoStream(TestSpec):
312318
name = "include-iostream"
313319
message = "Do not include iostream. Use O2 logging instead."
314320
rationale = "Performance. Avoid injection of static constructors. Consistent logging."
315-
references = [Reference.LLVM, Reference.LINTER]
321+
references = [Reference.LLVM, Reference.LINTER_1]
316322
suffixes = [".h", ".cxx"]
317323

318324
def test_line(self, line: str) -> bool:
@@ -327,7 +333,7 @@ class TestUsingStd(TestSpec):
327333
name = "import-std-name"
328334
message = "Do not import names from the std namespace in headers."
329335
rationale = "Code safety. Avoid namespace pollution with common names."
330-
references = [Reference.LINTER]
336+
references = [Reference.LINTER_1]
331337
suffixes = [".h"]
332338

333339
def test_line(self, line: str) -> bool:
@@ -342,7 +348,7 @@ class TestUsingDirective(TestSpec):
342348
name = "using-directive"
343349
message = "Do not put using directives at global scope in headers."
344350
rationale = "Code safety. Avoid namespace pollution."
345-
references = [Reference.O2, Reference.ISO_CPP, Reference.LLVM, Reference.GOOGLE, Reference.LINTER]
351+
references = [Reference.O2, Reference.ISO_CPP, Reference.LLVM, Reference.GOOGLE, Reference.LINTER_1]
346352
suffixes = [".h"]
347353

348354
def test_line(self, line: str) -> bool:
@@ -357,7 +363,7 @@ class TestStdPrefix(TestSpec):
357363
name = "std-prefix"
358364
message = "Use std:: prefix for names from the std namespace."
359365
rationale = "Code clarity, safety and portability. Avoid ambiguity (e.g. abs)."
360-
references = [Reference.LLVM, Reference.LINTER]
366+
references = [Reference.LLVM, Reference.LINTER_1]
361367
suffixes = [".h", ".cxx", ".C"]
362368
prefix_bad = r"[^\w:\.\"]"
363369
patterns = [
@@ -401,7 +407,7 @@ class TestRootEntity(TestSpec):
401407
name = "root/entity"
402408
message = "Replace ROOT entities with equivalents from standard C++ or from O2."
403409
rationale = "Code simplicity and maintainability. O2 is not a ROOT code."
404-
references = [Reference.ISO_CPP, Reference.LINTER, Reference.PY_ZEN]
410+
references = [Reference.ISO_CPP, Reference.LINTER_1, Reference.PY_ZEN]
405411
suffixes = [".h", ".cxx"]
406412

407413
def file_matches(self, path: str) -> bool:
@@ -427,7 +433,7 @@ class TestRootLorentzVector(TestSpec):
427433
"Use std::array with RecoDecay methods or the ROOT::Math::LorentzVector template instead."
428434
)
429435
rationale = "Performance. Use up-to-date tools."
430-
references = []
436+
references = [Reference.LINTER_2]
431437
suffixes = [".h", ".cxx"]
432438

433439
def test_line(self, line: str) -> bool:
@@ -443,7 +449,7 @@ class TestPi(TestSpec):
443449
name = "external-pi"
444450
message = "Use the PI constant (and its multiples and fractions) defined in o2::constants::math."
445451
rationale = "Code maintainability."
446-
references = [Reference.LINTER, Reference.PY_ZEN]
452+
references = [Reference.LINTER_1, Reference.PY_ZEN]
447453
suffixes = [".h", ".cxx"]
448454

449455
def file_matches(self, path: str) -> bool:
@@ -463,7 +469,7 @@ class TestTwoPiAddSubtract(TestSpec):
463469
name = "two-pi-add-subtract"
464470
message = "Use RecoDecay::constrainAngle to restrict angle to a given range."
465471
rationale = "Code maintainability and safety. Use existing tools."
466-
references = [Reference.ISO_CPP, Reference.LINTER, Reference.PY_ZEN]
472+
references = [Reference.ISO_CPP, Reference.LINTER_1, Reference.PY_ZEN]
467473
suffixes = [".h", ".cxx"]
468474

469475
def test_line(self, line: str) -> bool:
@@ -484,7 +490,7 @@ class TestPiMultipleFraction(TestSpec):
484490
name = "pi-multiple-fraction"
485491
message = "Use multiples/fractions of PI defined in o2::constants::math."
486492
rationale = "Code maintainability."
487-
references = [Reference.LINTER, Reference.PY_ZEN]
493+
references = [Reference.LINTER_1, Reference.PY_ZEN]
488494
suffixes = [".h", ".cxx"]
489495

490496
def test_line(self, line: str) -> bool:
@@ -507,7 +513,7 @@ class TestPdgDatabase(TestSpec):
507513
"Use o2::constants::physics::Mass... or Service<o2::framework::O2DatabasePDG> instead."
508514
)
509515
rationale = "Performance."
510-
references = [Reference.LINTER]
516+
references = [Reference.LINTER_1]
511517
suffixes = [".h", ".cxx"]
512518

513519
def file_matches(self, path: str) -> bool:
@@ -526,7 +532,7 @@ class TestPdgExplicitCode(TestSpec):
526532
name = "pdg/explicit-code"
527533
message = "Avoid hard-coded PDG codes. Use named values from PDG_t or o2::constants::physics::Pdg instead."
528534
rationale = "Code comprehensibility, readability, maintainability and safety."
529-
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER]
535+
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER_1]
530536
suffixes = [".h", ".cxx", ".C"]
531537

532538
def test_line(self, line: str) -> bool:
@@ -549,7 +555,7 @@ class TestPdgExplicitMass(TestSpec):
549555
name = "pdg/explicit-mass"
550556
message = "Avoid hard-coded particle masses. Use o2::constants::physics::Mass... instead."
551557
rationale = "Code comprehensibility, readability, maintainability and safety."
552-
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER]
558+
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER_2]
553559
suffixes = [".h", ".cxx"]
554560
masses: "list[str]" = [] # list of mass values to detect
555561

@@ -593,7 +599,7 @@ class TestPdgKnownMass(TestSpec):
593599
name = "pdg/known-mass"
594600
message = "Use o2::constants::physics::Mass... instead of calling a database method for a known PDG code."
595601
rationale = "Performance."
596-
references = [Reference.LINTER]
602+
references = [Reference.LINTER_1]
597603
suffixes = [".h", ".cxx", ".C"]
598604

599605
def test_line(self, line: str) -> bool:
@@ -612,7 +618,7 @@ class TestLogging(TestSpec):
612618
name = "logging"
613619
message = "Use O2 logging (LOG, LOGF, LOGP)."
614620
rationale = "Logs easy to read and process."
615-
references = [Reference.LINTER]
621+
references = [Reference.LINTER_1]
616622
suffixes = [".h", ".cxx"]
617623

618624
def file_matches(self, path: str) -> bool:
@@ -653,7 +659,7 @@ class TestConstRefInSubscription(TestSpec):
653659
name = "const-ref-in-process"
654660
message = "Use constant references for table subscriptions in process functions."
655661
rationale = "Performance, code comprehensibility and safety."
656-
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER]
662+
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER_1]
657663
suffixes = [".cxx"]
658664
per_line = False
659665

@@ -731,7 +737,7 @@ class TestWorkflowOptions(TestSpec):
731737
"Use process function switches or metadata instead."
732738
)
733739
rationale = "Not supported on AliHyperloop."
734-
references = [Reference.LINTER]
740+
references = [Reference.LINTER_1]
735741
suffixes = [".cxx"]
736742
per_line = False
737743

@@ -767,10 +773,10 @@ class TestMagicNumber(TestSpec):
767773
name = "magic-number"
768774
message = "Avoid magic numbers in expressions. Assign the value to a clearly named variable or constant."
769775
rationale = "Code comprehensibility, maintainability and safety."
770-
references = [Reference.O2, Reference.ISO_CPP]
776+
references = [Reference.O2, Reference.ISO_CPP, Reference.LINTER_2]
771777
suffixes = [".h", ".cxx", ".C"]
772778
pattern_compare = r"([<>]=?|[!=]=)"
773-
pattern_number = r"[\+-]?([\d\.]+)f?"
779+
pattern_number = r"[\+-]?([\d\.]+(e[\+-]?\d+)?)f?"
774780

775781
def test_line(self, line: str) -> bool:
776782
if is_comment_cpp(line):
@@ -779,7 +785,7 @@ def test_line(self, line: str) -> bool:
779785
iterators = re.finditer(
780786
rf" {self.pattern_compare} {self.pattern_number}|\W{self.pattern_number} {self.pattern_compare} ", line
781787
)
782-
matches = [(it.start(), it.group(2), it.group(3)) for it in iterators]
788+
matches = [(it.start(), it.group(2), it.group(4)) for it in iterators]
783789
if not matches:
784790
return True
785791
# Ignore matches inside strings.
@@ -805,7 +811,7 @@ class TestDocumentationFile(TestSpec):
805811
name = "doc/file"
806812
message = "Provide mandatory file documentation."
807813
rationale = "Code comprehensibility. Collaboration."
808-
references = [Reference.O2, Reference.LINTER]
814+
references = [Reference.O2, Reference.LINTER_1]
809815
suffixes = [".h", ".cxx", ".C"]
810816
per_line = False
811817

@@ -849,7 +855,7 @@ def test_file(self, path: str, content) -> bool:
849855
# Reference: https://rawgit.com/AliceO2Group/CodingGuidelines/master/naming_formatting.html
850856

851857
rationale_names = "Code readability, comprehensibility and searchability."
852-
references_names = [Reference.O2, Reference.LINTER]
858+
references_names = [Reference.O2, Reference.LINTER_1]
853859

854860

855861
class TestNameFunctionVariable(TestSpec):
@@ -989,29 +995,23 @@ class TestNameConstant(TestSpec):
989995
references = references_names
990996
suffixes = [".h", ".cxx", ".C"]
991997

998+
def __init__(self) -> None:
999+
super().__init__()
1000+
keyword = r"(.+ )" # e.g. "static "
1001+
type_val = r"([\w:<>+\-*\/, ]+ )" # value type e.g. "std::array<Type, n + 1> "
1002+
prefix = r"(\w+::)" # prefix with namespace or class, e.g. "MyClass::"
1003+
name_val = r"(\w+)" # name of the constant
1004+
array = r"(\[.*\])" # array declaration: "[...]"
1005+
assignment = r"( =|\(\d|{)" # value assignment, e.g. " = 2", " = expression", "(2)", "{2}", "{{...}}"
1006+
self.pattern = re.compile(rf"{keyword}?constexpr {type_val}?{prefix}*{name_val}{array}?{assignment}")
1007+
9921008
def test_line(self, line: str) -> bool:
9931009
if is_comment_cpp(line):
9941010
return True
9951011
line = remove_comment_cpp(line)
996-
words = line.split()
997-
if "constexpr" not in words or "=" not in words:
1012+
if not (match := self.pattern.match(line)):
9981013
return True
999-
# Extract constant name.
1000-
words = words[: words.index("=")] # keep only words before "="
1001-
constant_name = words[-1] # last word before "="
1002-
if (
1003-
constant_name.endswith("]") and "[" not in constant_name
1004-
): # it's an array and we do not have the name before "[" here
1005-
opens_brackets = ["[" in w for w in words]
1006-
if not any(opens_brackets): # The opening "[" is not on this line. We have to give up.
1007-
return True
1008-
constant_name = words[opens_brackets.index(True)] # the name is in the first element with "["
1009-
if "[" in constant_name: # Remove brackets for arrays.
1010-
constant_name = constant_name[: constant_name.index("[")]
1011-
if "::" in constant_name: # Remove the class prefix for methods.
1012-
constant_name = constant_name.split("::")[-1]
1013-
if "#" in constant_name: # Remove "#" for strings in macros.
1014-
constant_name = constant_name[: constant_name.index("#")]
1014+
constant_name = match.group(4)
10151015
# The actual test comes here.
10161016
if constant_name.startswith("k") and len(constant_name) > 1: # exception for special constants
10171017
constant_name = constant_name[1:] # test the name without "k"
@@ -1149,15 +1149,10 @@ class TestNameUpperCamelCase(TestSpec):
11491149
def test_line(self, line: str) -> bool:
11501150
if is_comment_cpp(line):
11511151
return True
1152-
if not line.startswith(f"{self.keyword} "):
1152+
if not (match := re.match(rf"{self.keyword}( (class|struct))? (\w+)", line)):
11531153
return True
11541154
# Extract object name.
1155-
words = line.split()
1156-
if not words[1].isalnum(): # "struct : ...", "enum { ..."
1157-
return True
1158-
object_name = words[1]
1159-
if object_name in ("class", "struct") and len(words) > 2: # enum class ... or enum struct
1160-
object_name = words[2]
1155+
object_name = match.group(3)
11611156
# The actual test comes here.
11621157
return is_upper_camel_case(object_name)
11631158

@@ -1216,7 +1211,7 @@ class TestNameFilePython(TestSpec):
12161211
name = "name/file-python"
12171212
message = "Use snake_case for names of Python files."
12181213
rationale = rationale_names
1219-
references = [Reference.LINTER, Reference.PY_PEP8]
1214+
references = [Reference.LINTER_1, Reference.PY_PEP8]
12201215
suffixes = [".py", ".ipynb"]
12211216
per_line = False
12221217

@@ -1278,7 +1273,7 @@ class TestNameTask(TestSpec):
12781273
name = "name/o2-task"
12791274
message = "Specify task name only when it cannot be derived from the struct name. Only append to the default name."
12801275
rationale = f"{rationale_names} Correspondence struct ↔ device."
1281-
references = [Reference.LINTER]
1276+
references = [Reference.LINTER_1]
12821277
suffixes = [".cxx"]
12831278
per_line = False
12841279

@@ -1423,7 +1418,7 @@ class TestNameFileWorkflow(TestSpec):
14231418
'(Class implementation files should be in "Core" directories.)'
14241419
)
14251420
rationale = f"{rationale_names} Correspondence file ↔ struct."
1426-
references = [Reference.LINTER]
1421+
references = [Reference.LINTER_1]
14271422
suffixes = [".cxx"]
14281423
per_line = False
14291424

@@ -1466,7 +1461,7 @@ class TestNameConfigurable(TestSpec):
14661461
"for the struct member as for the JSON string. (Declare the type and names on the same line.)"
14671462
)
14681463
rationale = f"{rationale_names} Correspondence C++ code ↔ JSON."
1469-
references = [Reference.O2, Reference.LINTER]
1464+
references = [Reference.O2, Reference.LINTER_1]
14701465
suffixes = [".h", ".cxx"]
14711466

14721467
def file_matches(self, path: str) -> bool:
@@ -1475,33 +1470,21 @@ def file_matches(self, path: str) -> bool:
14751470
def test_line(self, line: str) -> bool:
14761471
if is_comment_cpp(line):
14771472
return True
1478-
if not line.startswith("Configurable"):
1479-
return True
1473+
if not (match := re.match(r"((o2::)?framework::)?Configurable(\w+|<.+>) (\w+)( = )?{([^,{]+),", line)):
1474+
return not re.match(r"((o2::)?framework::)?Configurable", line)
14801475
# Extract Configurable name.
1481-
words = line.split()
1482-
if len(words) < 2:
1483-
return False
1484-
if len(words) > 2 and words[2] == "=": # expecting Configurable... nameCpp = {"nameJson",
1485-
name_cpp = words[1] # nameCpp
1486-
name_json = words[3][1:] # expecting "nameJson",
1487-
else:
1488-
names = words[1].split("{") # expecting Configurable... nameCpp{"nameJson",
1489-
if len(names) < 2:
1490-
return False
1491-
name_cpp = names[0] # nameCpp
1492-
name_json = names[1] # expecting "nameJson",
1493-
if not name_json:
1494-
return False
1476+
name_cpp = match.group(4) # nameCpp
1477+
name_json = match.group(6) # expecting "nameJson"
14951478
if name_json[0] != '"': # JSON name is not a literal string.
14961479
return True
1497-
name_json = name_json.strip('",') # expecting nameJson
1480+
name_json = name_json[1:-1] # Strip away quotation marks.
14981481
# The actual test comes here.
14991482
return is_lower_camel_case(name_cpp) and name_cpp == name_json
15001483

15011484

15021485
# PWG-HF
15031486

1504-
references_hf = [Reference.LINTER, Reference.PWG_HF]
1487+
references_hf = [Reference.LINTER_1, Reference.PWG_HF]
15051488

15061489

15071490
class TestHfNameStructClass(TestSpec):
@@ -1747,7 +1730,7 @@ def main():
17471730
ref_names = []
17481731
for test in tests:
17491732
if any(n > 0 for n in (test.n_issues, test.n_disabled, test.n_tolerated, n_files_bad[test.name])):
1750-
ref_ids = [ref.value for ref in test.references]
1733+
ref_ids = sorted(ref.value for ref in test.references)
17511734
ref_names += test.references
17521735
print(
17531736
f"{test.name}{' ' * (len_max - len(test.name))}\t{test.n_issues}\t{test.n_tolerated}"
@@ -1802,7 +1785,7 @@ def main():
18021785
print("Skipping writing in GITHUB_OUTPUT.")
18031786

18041787
# Print tips.
1805-
print("\nTip: You can run the O2 linter locally with: python3 Scripts/o2_linter.py <files>")
1788+
print("\nTip: You can run the O2 linter locally from the O2Physics directory with: python3 Scripts/o2_linter.py <files>")
18061789

18071790
if not passed:
18081791
sys.exit(1)

0 commit comments

Comments
 (0)