Skip to main content

drep/languages/
definitions.rs

1//! The registered languages.
2//!
3//! Adding a language is an entry here plus, if it has one, a tool output parser.
4//! No control flow anywhere else in drep changes.
5//!
6//! `config_files` is what makes a tool run at all: drep checks a project against
7//! the style that project has *chosen*, so a repo with no eslint config gets no
8//! eslint findings rather than a wall of default-preset complaints.
9
10use super::spec::{DEFAULT_TOOL_TIMEOUT_SECS, LanguageSupport, ToolSpec};
11
12/// Build outputs shared by the JVM languages. Gradle writes `build` and
13/// `.gradle`, Maven writes `target`. Declared once rather than repeated across
14/// four entries that can never legitimately disagree.
15///
16/// The set is global in effect: `files::is_ignored_dir` consults the union of
17/// every language's vendored directories, so an entry here skips the directory
18/// in a repository with no JVM code at all. `out` is therefore deliberately
19/// absent: IntelliJ's build output is nearly always gitignored anyway (which
20/// the walker honors on its own), while the name is generic enough that a
21/// checked-in `out/` of real sources in some other ecosystem would be silently
22/// dropped from review.
23static JVM_VENDORED_DIRS: &[&str] = &["build", ".gradle", "target"];
24
25/// Python deterministic checker.
26pub static RUFF: ToolSpec = ToolSpec {
27    name: "ruff",
28    command: &["ruff", "check", "--output-format", "json"],
29    local_paths: &["venv/bin/ruff", ".venv/bin/ruff"],
30    config_files: &["pyproject.toml", "ruff.toml", ".ruff.toml"],
31    config_flag: None,
32    output_format: "json",
33    diagnostics_stream: "stdout",
34    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
35    timeout_context: None,
36    establishes_compilation: false,
37    serial_in_repository: false,
38    accepts_files: true,
39};
40
41/// JavaScript deterministic checker.
42pub static ESLINT: ToolSpec = ToolSpec {
43    name: "eslint",
44    command: &["eslint", "--format", "json"],
45    local_paths: &["node_modules/.bin/eslint"],
46    config_files: &[
47        "eslint.config.js",
48        "eslint.config.mjs",
49        "eslint.config.cjs",
50        ".eslintrc",
51        ".eslintrc.js",
52        ".eslintrc.cjs",
53        ".eslintrc.json",
54        ".eslintrc.yml",
55        ".eslintrc.yaml",
56    ],
57    config_flag: None,
58    output_format: "json",
59    diagnostics_stream: "stdout",
60    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
61    timeout_context: None,
62    establishes_compilation: false,
63    serial_in_repository: false,
64    accepts_files: true,
65};
66
67/// TypeScript's compiler-as-checker. Streams diagnostics to stdout.
68pub static TSC: ToolSpec = ToolSpec {
69    name: "tsc",
70    command: &["tsc", "--noEmit", "--pretty", "false"],
71    local_paths: &["node_modules/.bin/tsc"],
72    config_files: &["tsconfig.json"],
73    config_flag: None,
74    output_format: "tsc",
75    diagnostics_stream: "stdout",
76    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
77    timeout_context: None,
78    establishes_compilation: true,
79    serial_in_repository: false,
80    // Passing source files makes tsc ignore tsconfig.json. Run the configured
81    // project and filter its diagnostics back to the requested files.
82    accepts_files: false,
83};
84
85/// Go formatting checker - lists files whose formatting drifts from `gofmt`'s.
86///
87/// `go.mod` is the marker that this is a Go module at all; gofmt has no
88/// config of its own because its formatting is not configurable.
89pub static GOFMT: ToolSpec = ToolSpec {
90    name: "gofmt",
91    command: &["gofmt", "-l"],
92    local_paths: &[],
93    config_files: &["go.mod"],
94    config_flag: None,
95    output_format: "lines",
96    diagnostics_stream: "stdout",
97    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
98    timeout_context: None,
99    establishes_compilation: false,
100    serial_in_repository: false,
101    accepts_files: true,
102};
103
104/// `go vet`. Streams diagnostics to stderr.
105///
106/// Not -json: that only emits JSON once the package compiles, and a package
107/// that does not compile is exactly when vet has the most to say.
108pub static GO_VET: ToolSpec = ToolSpec {
109    name: "go vet",
110    command: &["go", "vet"],
111    local_paths: &[],
112    config_files: &["go.mod"],
113    config_flag: None,
114    output_format: "position",
115    diagnostics_stream: "stderr",
116    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
117    timeout_context: None,
118    establishes_compilation: true,
119    serial_in_repository: false,
120    accepts_files: true,
121};
122
123/// Rust linter - emits structured JSON via cargo's message-format.
124pub static CLIPPY: ToolSpec = ToolSpec {
125    name: "clippy",
126    command: &["cargo", "clippy", "--message-format", "json", "--quiet"],
127    local_paths: &[],
128    config_files: &["Cargo.toml"],
129    config_flag: None,
130    output_format: "cargo",
131    diagnostics_stream: "stdout",
132    // Cargo's build lock is acquired by Cargo itself, so its wait is part of
133    // the child process. Allow the same long-running ceiling as an LLM review
134    // rather than failing a whole gate at the generic two-minute tool limit.
135    timeout_secs: 1_800,
136    timeout_context: Some(", including its Cargo build-lock wait"),
137    establishes_compilation: true,
138    serial_in_repository: true,
139    // `cargo clippy` checks a crate, not files: a path argument is rejected
140    // with "unexpected argument". See `ToolSpec::accepts_files`.
141    accepts_files: false,
142};
143
144/// Java linter. Emits SARIF 2.1.0 on stdout; its startup banner goes to stderr.
145///
146/// `-c` is not in `command`: checkstyle refuses to run without a ruleset and
147/// which one a project uses is exactly what `config_files` discovers, so the
148/// path is appended by `config_flag`. Bare, it exits 1 with "Must specify a
149/// config XML".
150pub static CHECKSTYLE: ToolSpec = ToolSpec {
151    name: "checkstyle",
152    command: &["checkstyle", "-f", "sarif"],
153    local_paths: &[],
154    config_files: &[
155        "checkstyle.xml",
156        ".checkstyle.xml",
157        "config/checkstyle/checkstyle.xml",
158        "gradle/config/checkstyle/checkstyle.xml",
159    ],
160    config_flag: Some("-c"),
161    output_format: "sarif",
162    diagnostics_stream: "stdout",
163    // A JVM start plus a full reflections scan of the check registry, on every
164    // invocation. Two minutes is enough but not generous on a cold page cache.
165    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
166    timeout_context: None,
167    // It parses; it does not compile. A clean run says nothing about whether
168    // javac would accept the file.
169    establishes_compilation: false,
170    serial_in_repository: false,
171    accepts_files: true,
172};
173
174/// Kotlin linter and formatter, run in lint-only mode.
175///
176/// `--log-level=none` is load-bearing: ktlint writes "Lint has found errors
177/// than can be autocorrected" to **stdout**, ahead of the JSON, and the parser
178/// would reject the whole run as unparseable rather than report the findings.
179pub static KTLINT: ToolSpec = ToolSpec {
180    name: "ktlint",
181    command: &["ktlint", "--log-level=none", "--reporter=json"],
182    local_paths: &[],
183    // ktlint reads `.editorconfig` and nothing else. A Kotlin repo without one
184    // has not chosen ktlint's defaults, so it is skipped.
185    config_files: &[".editorconfig"],
186    config_flag: None,
187    output_format: "ktlint",
188    diagnostics_stream: "stdout",
189    timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
190    timeout_context: None,
191    establishes_compilation: false,
192    serial_in_repository: false,
193    accepts_files: true,
194};
195
196/// Python language entry.
197pub static PYTHON: LanguageSupport = LanguageSupport {
198    name: "python",
199    display_name: "Python",
200    extensions: &[".py"],
201    tools: &[&RUFF],
202    conventions: &[
203        "Follows PEP 8 naming and structure",
204        "Type hints on public APIs, and correct use of Optional/None",
205        "Context managers for resources rather than manual cleanup",
206        "Mutable default arguments, and late-binding closures in loops",
207    ],
208    vendored_dirs: &["__pycache__", "venv", ".venv", "env", ".tox", ".eggs"],
209};
210
211/// JavaScript language entry.
212pub static JAVASCRIPT: LanguageSupport = LanguageSupport {
213    name: "javascript",
214    display_name: "JavaScript",
215    extensions: &[".js", ".jsx", ".mjs", ".cjs"],
216    tools: &[&ESLINT],
217    conventions: &[
218        "Unhandled promise rejections and missing await",
219        "Sequential awaits in a loop where the work is independent",
220        "var versus let/const, and accidental global scope",
221        "Equality coercion (== versus ===)",
222    ],
223    vendored_dirs: &["node_modules", ".next", ".nuxt"],
224};
225
226/// TypeScript language entry.
227pub static TYPESCRIPT: LanguageSupport = LanguageSupport {
228    name: "typescript",
229    display_name: "TypeScript",
230    extensions: &[".ts", ".tsx", ".mts", ".cts"],
231    tools: &[&ESLINT, &TSC],
232    conventions: &[
233        "`any` where a real type is available, and unsafe casts",
234        "Unhandled promise rejections and missing await",
235        "Non-null assertions (!) that hide a genuine null case",
236        "Sequential awaits in a loop where the work is independent",
237    ],
238    vendored_dirs: &["node_modules", ".next", ".nuxt"],
239};
240
241/// Go language entry.
242pub static GO: LanguageSupport = LanguageSupport {
243    name: "go",
244    display_name: "Go",
245    extensions: &[".go"],
246    tools: &[&GOFMT, &GO_VET],
247    conventions: &[
248        "Errors ignored rather than checked and wrapped",
249        "Goroutine leaks, and writes to a channel nobody reads",
250        "defer inside a loop, and defer that never runs",
251        "Data races on shared state without synchronisation",
252    ],
253    vendored_dirs: &["vendor"],
254};
255
256/// Rust language entry.
257pub static RUST_LANG: LanguageSupport = LanguageSupport {
258    name: "rust",
259    display_name: "Rust",
260    extensions: &[".rs"],
261    tools: &[&CLIPPY],
262    conventions: &[
263        "unwrap/expect on values that can legitimately be None or Err",
264        "unsafe blocks, and whether their invariants are documented",
265        "Unnecessary clones and allocations in hot paths",
266        "Send/Sync correctness for types crossing threads",
267    ],
268    vendored_dirs: &["target"],
269};
270
271/// Java language entry.
272pub static JAVA: LanguageSupport = LanguageSupport {
273    name: "java",
274    display_name: "Java",
275    extensions: &[".java"],
276    tools: &[&CHECKSTYLE],
277    conventions: &[
278        "Resources closed on every path, and try-with-resources where it applies",
279        "Null handling: Optional versus a nullable return, and unchecked dereferences",
280        "equals/hashCode/compareTo consistency, and mutable state in a shared object",
281        "Exceptions swallowed or logged and rethrown, losing the original cause",
282        "Concurrency: unsynchronised shared state, and non-thread-safe fields on a singleton",
283    ],
284    vendored_dirs: JVM_VENDORED_DIRS,
285};
286
287/// Kotlin language entry.
288///
289/// `.kts` covers both scratch scripts and `build.gradle.kts`, which
290/// `Path::extension` reports as `kts` rather than `gradle.kts`.
291pub static KOTLIN: LanguageSupport = LanguageSupport {
292    name: "kotlin",
293    display_name: "Kotlin",
294    extensions: &[".kt", ".kts"],
295    tools: &[&KTLINT],
296    conventions: &[
297        "Platform types from Java interop dereferenced without a null check",
298        "runBlocking on a coroutine path, and scopes that outlive their work",
299        "!! where the null case is real, and lateinit read before assignment",
300        "data class equality over mutable properties",
301    ],
302    vendored_dirs: JVM_VENDORED_DIRS,
303};
304
305/// Scala language entry.
306///
307/// No deterministic tool. scalafmt and scalafix are both build-plugin-first
308/// here, and neither has a standalone CLI drep can invoke the way it invokes
309/// ruff. The semantic half needs none, so the language is registered anyway
310/// rather than leaving `.scala` unreadable.
311pub static SCALA: LanguageSupport = LanguageSupport {
312    name: "scala",
313    display_name: "Scala",
314    extensions: &[".scala", ".sc"],
315    tools: &[],
316    conventions: &[
317        "Partial functions and non-exhaustive matches",
318        "Option/Either handling versus get and head on an empty collection",
319        "Implicits whose resolution is not obvious at the call site",
320        "Futures without an explicit ExecutionContext, and blocking inside one",
321    ],
322    vendored_dirs: JVM_VENDORED_DIRS,
323};
324
325/// Groovy language entry.
326///
327/// `.gradle` is here because a Gradle build script is Groovy, and a change to
328/// one is exactly the kind of thing worth a second read. No deterministic
329/// tool: CodeNarc is a build plugin rather than a CLI.
330pub static GROOVY: LanguageSupport = LanguageSupport {
331    name: "groovy",
332    display_name: "Groovy",
333    extensions: &[".groovy", ".gradle"],
334    tools: &[],
335    conventions: &[
336        "Dynamic dispatch where a typed call would fail at compile time",
337        "Gradle configuration-time work that belongs in a task action",
338        "Dependency and plugin versions pinned versus floating",
339        "String interpolation of values that should be escaped",
340    ],
341    vendored_dirs: JVM_VENDORED_DIRS,
342};
343
344/// Every registered language, in registration order.
345pub static ALL_LANGUAGES: &[&LanguageSupport] = &[
346    &PYTHON,
347    &JAVASCRIPT,
348    &TYPESCRIPT,
349    &GO,
350    &RUST_LANG,
351    &JAVA,
352    &KOTLIN,
353    &SCALA,
354    &GROOVY,
355];