gen-lints.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env python3
  2. # Copyright Materialize, Inc. and contributors. All rights reserved.
  3. #
  4. # Use of this software is governed by the Business Source License
  5. # included in the LICENSE file at the root of this repository.
  6. #
  7. # As of the Change Date specified in that file, in accordance with
  8. # the Business Source License, use of this software will be governed
  9. # by the Apache License, Version 2.0.
  10. """Regenerate lint annotations on all root Cargo workspace files and check that
  11. all crates inherit their lints from the workspace."""
  12. import argparse
  13. import json
  14. import os
  15. import subprocess
  16. import tempfile
  17. from pathlib import Path
  18. import toml
  19. ALLOW_RUST_LINTS = [
  20. # Allows us to allow/deny new lints and support older versions of rust/clippy.
  21. "unknown_lints",
  22. "non_local_definitions",
  23. ]
  24. ALLOW_RUST_DOC_LINTS = []
  25. ALLOW_CLIPPY_LINTS = [
  26. # The style and complexity lints frustrated too many engineers and caused
  27. # more bikeshedding than they saved. These lint categories are largely a
  28. # matter of opinion. A few of the worthwhile lints in these categories are
  29. # reenabled in `DENY_LINTS` below.
  30. ("style", -1),
  31. ("complexity", -1),
  32. # clippy::large_enum_variant complains when enum variants have divergent
  33. # sizes, as the size of an enum is determined by the size of its largest
  34. # element. Obeying this lint is nearly always a premature optimization,
  35. # and the suggested solution—boxing the large variant—might actually result
  36. # in slower code due to the allocation.
  37. ("large_enum_variant", 0),
  38. # A specialization of `large_enum_variant`; similar arguments apply.
  39. ("result_large_err", 0),
  40. # clippy::mutable_key_type disallows using internally mutable types as keys
  41. # in `HashMap`, because their order could change. This is a good lint in
  42. # principle, but its current implementation is too strict -- it disallows
  43. # anything containing an `Arc` or `Rc`, for example.
  44. ("mutable_key_type", 0),
  45. # Unstable sort is not strictly better than sort, notably on partially
  46. # sorted inputs.
  47. ("stable_sort_primitive", 0),
  48. # This lint has false positives where the pattern cannot be avoided without
  49. # cloning the key used in the map.
  50. ("map_entry", 0),
  51. # It is unclear if the performance gain from using `Box::default` instead of
  52. # `Box::new` is meaningful; and the lint can result in inconsistencies
  53. # when some types implement `Default` and others do not.
  54. # TODO(guswynn): benchmark the performance gain.
  55. ("box_default", 0),
  56. # This suggestion misses the point of `.drain(..).collect()` entirely:
  57. # to keep the capacity of the original collection the same.
  58. ("drain_collect", 0),
  59. ]
  60. WARN_CLIPPY_LINTS = [
  61. # Comparison of a bool with `true` or `false` is indeed clearer as `x` or
  62. # `!x`.
  63. "bool_comparison",
  64. # Rewrite `x.clone()` to `Arc::clone(&x)`.
  65. # This clarifies a significant amount of code by making it visually clear
  66. # when a clone is cheap.
  67. "clone_on_ref_ptr",
  68. # These can catch real bugs, because something that is expected (a cast, a
  69. # conversion, a statement) is not happening.
  70. "no_effect",
  71. "unnecessary_unwrap",
  72. # Prevent code using the `dbg!` macro from being merged to the main branch.
  73. #
  74. # To mark a debugging print as intentional (e.g., in a test), use
  75. # `println!("{:?}", obj)` instead.
  76. "dbg_macro",
  77. # Prevent code containing the `todo!` macro from being merged to the main
  78. # branch.
  79. #
  80. # To mark something as intentionally unimplemented, use the `unimplemented!`
  81. # macro instead.
  82. "todo",
  83. # Wildcard dependencies are, by definition, incorrect. It is impossible
  84. # to be compatible with all future breaking changes in a crate.
  85. #
  86. # TODO(parkmycar): Re-enable this lint when it's supported in Bazel.
  87. # "wildcard_dependencies",
  88. # Zero-prefixed literals may be incorrectly interpreted as octal literals.
  89. "zero_prefixed_literal",
  90. # Purely redundant tokens.
  91. "borrowed_box",
  92. "deref_addrof",
  93. "double_must_use",
  94. "double_parens",
  95. "extra_unused_lifetimes",
  96. "needless_borrow",
  97. "needless_question_mark",
  98. "needless_return",
  99. "redundant_pattern",
  100. "redundant_slicing",
  101. "redundant_static_lifetimes",
  102. "single_component_path_imports",
  103. "unnecessary_cast",
  104. "useless_asref",
  105. "useless_conversion",
  106. # Very likely to be confusing.
  107. "builtin_type_shadow",
  108. "duplicate_underscore_argument",
  109. # Purely redundant tokens; very likely to be confusing.
  110. "double_negations",
  111. # Purely redundant tokens; code is misleading.
  112. "unnecessary_mut_passed",
  113. # Purely redundant tokens; probably a mistake.
  114. "wildcard_in_or_patterns",
  115. # Transmuting between T and T* seems 99% likely to be buggy code.
  116. "crosspointer_transmute",
  117. # Confusing and likely buggy.
  118. "excessive_precision",
  119. "panicking_overflow_checks",
  120. # The `as` operator silently truncates data in many situations. It is very
  121. # difficult to assess whether a given usage of `as` is dangerous or not. So
  122. # ban it outright, to force usage of safer patterns, like `From` and
  123. # `TryFrom`.
  124. #
  125. # When absolutely essential (e.g., casting from a float to an integer), you
  126. # can attach `#[allow(clippy::as_conversion)]` to a single statement.
  127. "as_conversions",
  128. # Confusing.
  129. "match_overlapping_arm",
  130. # Confusing; possibly a mistake.
  131. "zero_divided_by_zero",
  132. # Probably a mistake.
  133. "must_use_unit",
  134. "suspicious_assignment_formatting",
  135. "suspicious_else_formatting",
  136. "suspicious_unary_op_formatting",
  137. # Legitimate performance impact.
  138. "mut_mutex_lock",
  139. "print_literal",
  140. "same_item_push",
  141. "useless_format",
  142. "write_literal",
  143. # Extra closures slow down compiles due to unnecessary code generation
  144. # that LLVM needs to optimize.
  145. "redundant_closure",
  146. "redundant_closure_call",
  147. "unnecessary_lazy_evaluations",
  148. # Provably either redundant or wrong.
  149. "partialeq_ne_impl",
  150. # This one is a debatable style nit, but it's so ingrained at this point
  151. # that we might as well keep it.
  152. "redundant_field_names",
  153. # Needless unsafe.
  154. "transmutes_expressible_as_ptr_casts",
  155. # Needless async.
  156. "unused_async",
  157. # Disallow the methods, macros, and types listed in clippy.toml;
  158. # see that file for rationale.
  159. "disallowed_methods",
  160. "disallowed_macros",
  161. "disallowed_types",
  162. # Implementing `From` gives you `Into` for free, but the reverse is not
  163. # true.
  164. "from_over_into",
  165. # We consistently don't use `mod.rs` files.
  166. "mod_module_files",
  167. "needless_pass_by_ref_mut",
  168. # Helps prevent bugs caused by wrong usage of `const` instead of `static`
  169. # to define global mutable values.
  170. "borrow_interior_mutable_const",
  171. "or_fun_call",
  172. ]
  173. MESSAGE_LINT_MISSING = (
  174. '{} does not contain a "lints" section. Please add: \n[lints]\nworkspace = true'
  175. )
  176. MESSAGE_LINT_INHERIT = "The lint section in {} does not inherit from the workspace, "
  177. EXCLUDE_CRATES = ["workspace-hack"]
  178. CHECK_CFGS = "bazel, stamped, coverage, nightly_doc_features, release, tokio_unstable"
  179. def main() -> None:
  180. parser = argparse.ArgumentParser()
  181. parser.add_argument(
  182. "--extra-dirs",
  183. action="append",
  184. default=[],
  185. )
  186. args = parser.parse_args()
  187. lint_config = [
  188. "\n" "# BEGIN LINT CONFIG\n",
  189. "# DO NOT EDIT. Automatically generated by bin/gen-lints.\n",
  190. "[workspace.lints.rust]\n",
  191. *(f'{lint} = "allow"\n' for lint in ALLOW_RUST_LINTS),
  192. f"unexpected_cfgs = {{ level = \"warn\", check-cfg = ['cfg({CHECK_CFGS})'] }}\n",
  193. "\n",
  194. "[workspace.lints.rustdoc]\n",
  195. *(f'{lint} = "allow"\n' for lint in ALLOW_RUST_DOC_LINTS),
  196. "\n",
  197. "[workspace.lints.clippy]\n",
  198. *(
  199. f'{lint} = {{ level = "allow", priority = {priority} }}\n'
  200. for (lint, priority) in ALLOW_CLIPPY_LINTS
  201. ),
  202. *(f'{lint} = "warn"\n' for lint in WARN_CLIPPY_LINTS),
  203. "# END LINT CONFIG\n",
  204. ]
  205. for workspace_root in [".", *args.extra_dirs]:
  206. workspace_cargo_toml = Path(f"{workspace_root}/Cargo.toml")
  207. # Make sure the workspace Cargo.toml files have the lint config.
  208. contents = workspace_cargo_toml.read_text().splitlines(keepends=True)
  209. try:
  210. # Overwrite existing lint configuration block.
  211. start = contents.index(lint_config[1]) - 2
  212. end = contents.index(lint_config[-1])
  213. new_contents = contents[:start] + lint_config + contents[end + 1 :]
  214. except ValueError:
  215. # No existing lint configuration block. Add a new one to the end
  216. # of the file.
  217. new_contents = contents + lint_config
  218. # Only write file if the content changed.
  219. if "".join(new_contents) != "".join(contents):
  220. tmp_file_path = None
  221. try:
  222. # Overwrite the file atomically so that there is never a half-written file.
  223. with tempfile.NamedTemporaryFile(
  224. "w", delete=False, dir=workspace_root, encoding="utf-8"
  225. ) as tmp_file:
  226. tmp_file.write("".join(new_contents))
  227. tmp_file_path = tmp_file.name
  228. os.replace(tmp_file_path, workspace_cargo_toml)
  229. except:
  230. if tmp_file_path:
  231. try:
  232. os.remove(tmp_file_path)
  233. except:
  234. pass
  235. raise
  236. # Make sure all of the crates in the workspace inherit their lints.
  237. metadata = json.loads(
  238. subprocess.check_output(
  239. [
  240. "cargo",
  241. "metadata",
  242. "--no-deps",
  243. "--format-version=1",
  244. f"--manifest-path={workspace_root}/Cargo.toml",
  245. ]
  246. )
  247. )
  248. cargo_toml_paths = (
  249. Path(package["manifest_path"])
  250. for package in metadata["packages"]
  251. if package["name"] not in EXCLUDE_CRATES
  252. )
  253. for cargo_file in cargo_toml_paths:
  254. cargo_toml = {}
  255. with cargo_file.open() as rcf:
  256. cargo_toml = toml.load(rcf)
  257. # Assert the Cargo.toml contains a "lints" section.
  258. assert "lints" in cargo_toml, MESSAGE_LINT_MISSING.format(cargo_file)
  259. # Assert the lints section contains a "workspace" key.
  260. assert cargo_toml["lints"].get("workspace"), MESSAGE_LINT_INHERIT.format(
  261. cargo_file
  262. )
  263. if __name__ == "__main__":
  264. main()