check_diagnostics.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env python3
  2. """Checks diagnostic use.
  3. Validates that each diagnostic declared with CARBON_DIAGNOSTIC_KIND is
  4. referenced by one (and only one) CARBON_DIAGNOSTIC.
  5. """
  6. __copyright__ = """
  7. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  8. Exceptions. See /LICENSE for license information.
  9. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  10. """
  11. import collections
  12. from concurrent import futures
  13. import itertools
  14. import os
  15. from pathlib import Path
  16. import re
  17. import sys
  18. from typing import Dict, List, NamedTuple, Set
  19. # Example or test diagnostics, ignored because they're expected to not pass.
  20. IGNORED = set(["MyDiagnostic", "TestDiagnostic", "TestDiagnosticNote"])
  21. class Loc(NamedTuple):
  22. """A location for a diagnostic."""
  23. def __str__(self) -> str:
  24. return f"{str(self.path)}:{self.line}"
  25. path: Path
  26. line: int
  27. def load_diagnostic_kind() -> Set[str]:
  28. """Returns the set of declared diagnostic kinds.
  29. This isn't validated for uniqueness because the compiler does that.
  30. """
  31. path = Path("toolchain/diagnostics/diagnostic_kind.def")
  32. content = path.read_text()
  33. decls = set(re.findall(r"CARBON_DIAGNOSTIC_KIND\((.+)\)", content))
  34. return decls.difference(IGNORED)
  35. def load_diagnostic_uses_in(
  36. path: Path,
  37. ) -> Dict[str, List[Loc]]:
  38. """Returns the path's CARBON_DIAGNOSTIC uses."""
  39. content = path.read_text()
  40. # Keep a line cursor so that we don't keep re-scanning the file.
  41. line = 1
  42. line_offset = 0
  43. found: Dict[str, List[Loc]] = collections.defaultdict(lambda: [])
  44. for m in re.finditer(r"CARBON_DIAGNOSTIC\(\s*(\w+),", content):
  45. diag = m.group(1)
  46. if diag in IGNORED:
  47. continue
  48. line += content.count("\n", line_offset, m.start())
  49. line_offset = m.start()
  50. found[diag].append(Loc(path, line))
  51. return found
  52. def load_diagnostic_uses() -> Dict[str, List[Loc]]:
  53. """Returns all CARBON_DIAGNOSTIC uses."""
  54. globs = itertools.chain(
  55. *[Path("toolchain").glob(f"**/*.{ext}") for ext in ("h", "cpp")]
  56. )
  57. with futures.ThreadPoolExecutor() as exec:
  58. results = exec.map(load_diagnostic_uses_in, globs)
  59. found: Dict[str, List[Loc]] = collections.defaultdict(lambda: [])
  60. for result in results:
  61. for diag, locations in result.items():
  62. found[diag].extend(locations)
  63. return found
  64. def check_uniqueness(uses: Dict[str, List[Loc]]) -> bool:
  65. """If any diagnostic is non-unique, prints an error and returns true."""
  66. has_errors = False
  67. for diag in sorted(uses.keys()):
  68. if len(uses[diag]) > 1:
  69. print(f"Non-unique diagnostic {diag}:", file=sys.stderr)
  70. for loc in uses[diag]:
  71. print(f" - {loc}", file=sys.stderr)
  72. has_errors = True
  73. return has_errors
  74. def check_unused(decls: Set[str], uses: Dict[str, List[Loc]]) -> bool:
  75. """If any diagnostic is unused, prints an error and returns true."""
  76. unused = decls.difference(uses.keys())
  77. if not unused:
  78. return False
  79. for diag in sorted(unused):
  80. print(f"Unused diagnostic: {diag}")
  81. return True
  82. def main() -> None:
  83. # Run from the repo root.
  84. os.chdir(Path(__file__).parents[2])
  85. decls = load_diagnostic_kind()
  86. uses = load_diagnostic_uses()
  87. if any([check_uniqueness(uses), check_unused(decls, uses)]):
  88. exit(1)
  89. if __name__ == "__main__":
  90. main()