drep/languages/spec.rs
1//! Language support contract shared by every registered language.
2//!
3//! drep analyzes a file in two layers, and this module is what keeps them free of
4//! per-language conditionals:
5//!
6//! - **Deterministic**: the project's own tools (ruff, eslint, gofmt, clippy).
7//! They are precise, so their findings can gate a commit.
8//! - **Semantic**: the LLM, told which language it is looking at. It reads any
9//! language without a parser, so it needs no per-language machinery beyond a
10//! prompt - which is why adding a language here is a data change, not a
11//! refactor.
12//!
13//! Deliberately free of heavyweight drep imports: the registry is consulted by
14//! file discovery (`drep.core.file_targets`), which analyzer packages import.
15
16pub const DEFAULT_TOOL_TIMEOUT_SECS: u64 = 120;
17
18/// A deterministic checker for one language.
19///
20/// Attributes:
21/// name: Tool name, used in logs and finding provenance.
22/// command: argv to run, minus the files. The first element is resolved
23/// against local_paths before PATH.
24/// local_paths: Repo-relative locations to prefer over PATH, so a project
25/// gets the version its own CI runs (node_modules/.bin/eslint rather
26/// than whatever is installed globally).
27/// config_files: Repo-relative paths that mean "this project has opted
28/// into this tool". A tool with none of them present is skipped: its
29/// defaults are not the project's chosen style, so running it anyway
30/// would invent findings the project never asked for.
31/// output_format: How to parse the tool's diagnostics into findings.
32/// diagnostics_stream: Which stream carries them. `go vet` writes to
33/// stderr, so reading only stdout would report every Go file clean.
34#[derive(Debug, Clone, PartialEq)]
35pub struct ToolSpec {
36 /// Tool name, used in logs and finding provenance.
37 pub name: &'static str,
38 /// argv to run, minus the files. The first element is resolved against `local_paths` before PATH.
39 pub command: &'static [&'static str],
40 /// Repo-relative locations to prefer over PATH, so a project gets the
41 /// version its own CI runs (`node_modules/.bin/eslint` rather than
42 /// whatever is installed globally).
43 pub local_paths: &'static [&'static str],
44 /// Repo-relative paths that mean "this project has opted into this tool".
45 /// A tool with none of them present is skipped: its defaults are not the
46 /// project's chosen style, so running it anyway would invent findings the
47 /// project never asked for.
48 pub config_files: &'static [&'static str],
49 /// How to parse the tool's diagnostics into findings.
50 pub output_format: &'static str,
51 /// Which stream carries them. `go vet` writes to stderr, so reading only
52 /// stdout would report every Go file clean.
53 pub diagnostics_stream: &'static str,
54 /// Wall-clock ceiling for the process. This includes any tool-owned lock
55 /// wait before analysis starts.
56 pub timeout_secs: u64,
57 /// Optional diagnostic suffix explaining a legitimate long wait.
58 pub timeout_context: Option<&'static str>,
59 /// A zero-exit run proves the checked files compiled/typechecked.
60 pub establishes_compilation: bool,
61 /// Whether invocations of this tool must be serialized within a repo.
62 pub serial_in_repository: bool,
63 /// Whether the tool accepts file paths as arguments.
64 ///
65 /// `cargo clippy` does not: it checks a *crate*, and a path argument is
66 /// rejected outright with "unexpected argument". Appending files to it
67 /// therefore made every run fail, so every Rust file came back
68 /// `Unavailable` and `drep check` exited 2 on any Rust repository - the
69 /// deterministic half for Rust never ran at all.
70 ///
71 /// A tool with `accepts_files: false` is invoked bare and reports on the
72 /// whole project, so its findings are filtered down to the files actually
73 /// being checked. Without that filter a commit gate would block on
74 /// pre-existing issues in code the commit never touched.
75 pub accepts_files: bool,
76}
77
78impl Default for ToolSpec {
79 fn default() -> Self {
80 Self {
81 name: "",
82 command: &[],
83 local_paths: &[],
84 config_files: &[],
85 output_format: "json",
86 diagnostics_stream: "stdout",
87 timeout_secs: DEFAULT_TOOL_TIMEOUT_SECS,
88 timeout_context: None,
89 establishes_compilation: false,
90 serial_in_repository: false,
91 accepts_files: true,
92 }
93 }
94}
95
96/// Everything drep needs to know about one language.
97///
98/// Attributes:
99/// name: Registry key (lowercase, e.g. "typescript").
100/// display_name: How the language is named to the LLM and to users.
101/// extensions: Lowercased suffixes this language owns, including the dot.
102/// tools: Deterministic checkers, in the order they should run.
103/// conventions: Language-specific guidance appended to the analysis
104/// prompt - the part that used to be hardcoded as PEP 8.
105/// vendored_dirs: Dependency and build directories this language creates,
106/// never descended into. Declared here rather than in a global list
107/// so adding a language stays a single-file change.
108#[derive(Debug, Default, Clone, PartialEq)]
109pub struct LanguageSupport {
110 /// Registry key (lowercase, e.g. `"typescript"`).
111 pub name: &'static str,
112 /// How the language is named to the LLM and to users.
113 pub display_name: &'static str,
114 /// Lowercased suffixes this language owns, including the dot.
115 pub extensions: &'static [&'static str],
116 /// Deterministic checkers, in the order they should run.
117 pub tools: &'static [&'static ToolSpec],
118 /// Language-specific guidance appended to the analysis prompt - the part
119 /// that used to be hardcoded as PEP 8.
120 pub conventions: &'static [&'static str],
121 /// Dependency and build directories this language creates, never
122 /// descended into. Declared here rather than in a global list so adding
123 /// a language stays a single-file change.
124 pub vendored_dirs: &'static [&'static str],
125}