Skip to main content

brief/minify/
mod.rs

1//! Code-block minifiers used by the LLM emit pass.
2//!
3//! v0.2 shipped JSON/JSONL. v0.3 adds Rust, Go, JavaScript/TypeScript, Java,
4//! C/C++, and SQL via hand-rolled tokenizers. Each minifier is fail-closed:
5//! any parse error returns `Err`, and the caller falls back to verbatim
6//! emission with a B0701 warning.
7//!
8//! Minification is required to be semantically lossless — the broad rule is
9//! `parse → minify → re-parse` produces structurally equal data. Where we
10//! lack a real parser we settle for tokenizer-equivalence and a hand-curated
11//! corpus of edge-case fixtures.
12
13pub mod c_common;
14pub mod c_cpp;
15pub mod go;
16pub mod java;
17pub mod javascript;
18pub mod json;
19pub mod rust;
20pub mod sql;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct MinifyError {
24    pub message: String,
25}
26
27impl MinifyError {
28    pub(crate) fn new(s: impl Into<String>) -> Self {
29        MinifyError { message: s.into() }
30    }
31}
32
33#[derive(Debug, Clone, Default)]
34pub struct MinifyOptions {
35    /// State 3 — `@minify-keep-comments`. When true the minifier strips
36    /// whitespace as usual but preserves comments, converting `//` line
37    /// comments to `/* */` block form (and recording a B0703 warning per
38    /// conversion). When false (default), comments are dropped entirely.
39    pub keep_comments: bool,
40}
41
42/// A successful minification produces a body plus zero-or-more
43/// per-conversion warnings (e.g. B0703 line-comment-converted notices).
44#[derive(Debug, Clone, Default)]
45pub struct MinifyOutput {
46    pub body: String,
47    pub warnings: Vec<MinifyWarning>,
48}
49
50impl MinifyOutput {
51    pub(crate) fn body(s: impl Into<String>) -> Self {
52        MinifyOutput {
53            body: s.into(),
54            warnings: Vec::new(),
55        }
56    }
57}
58
59/// Per-block warnings the minifier may produce. The LLM emit pass is
60/// responsible for translating these into stderr `warning[Bxxxx]: …` lines.
61#[derive(Debug, Clone)]
62pub enum MinifyWarning {
63    /// A `//` line comment was rewritten into `/* */` block-comment form so
64    /// it survived a single-line minification. The user owns the risk that
65    /// the comment body contains `*/`.
66    LineCommentConverted,
67}
68
69/// Returns true if `lang`, lowercased, is one of the minifiers shipped in
70/// this build. v0.3: json, jsonl, rust/rs, c/h, cpp/c++/cc/cxx/hpp/hxx,
71/// java, go, js/javascript, ts/typescript, sql.
72pub fn is_supported(lang: &str) -> bool {
73    matches!(
74        lang.to_ascii_lowercase().as_str(),
75        "json"
76            | "jsonl"
77            | "rust"
78            | "rs"
79            | "c"
80            | "h"
81            | "cpp"
82            | "c++"
83            | "cc"
84            | "cxx"
85            | "hpp"
86            | "hxx"
87            | "java"
88            | "go"
89            | "js"
90            | "javascript"
91            | "ts"
92            | "typescript"
93            | "sql"
94    )
95}
96
97/// If `lang` is a permanently-refused language (significant whitespace),
98/// return a one-line reason. Languages: python/py, yaml/yml, makefile/make/mk.
99pub fn refusal_reason(lang: &str) -> Option<&'static str> {
100    match lang.to_ascii_lowercase().as_str() {
101        "python" | "py" => Some("Python uses significant whitespace; cannot be safely minified"),
102        "yaml" | "yml" => Some("YAML uses significant whitespace; cannot be safely minified"),
103        "makefile" | "make" | "mk" => Some(
104            "Makefile syntax is whitespace-sensitive (tabs are significant); cannot be safely minified",
105        ),
106        _ => None,
107    }
108}
109
110/// Dispatch to the appropriate minifier by language tag. Caller must check
111/// `is_supported` first; an unsupported language returns Err.
112pub fn minify(lang: &str, source: &str, opts: &MinifyOptions) -> Result<MinifyOutput, MinifyError> {
113    match lang.to_ascii_lowercase().as_str() {
114        "json" => json::minify_json(source),
115        "jsonl" => json::minify_jsonl(source),
116        "rust" | "rs" => rust::minify(source, opts),
117        "c" | "h" => c_cpp::minify(source, opts, false),
118        "cpp" | "c++" | "cc" | "cxx" | "hpp" | "hxx" => c_cpp::minify(source, opts, true),
119        "java" => java::minify(source, opts),
120        "go" => go::minify(source, opts),
121        "js" | "javascript" | "ts" | "typescript" => javascript::minify(source, opts),
122        "sql" => sql::minify(source, opts),
123        other => Err(MinifyError::new(format!(
124            "no minifier registered for language `{}`",
125            other
126        ))),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn dispatch_json_and_aliases() {
136        let opts = MinifyOptions::default();
137        assert!(minify("json", "{}", &opts).is_ok());
138        assert!(minify("JSON", "{}", &opts).is_ok());
139        assert!(minify("jsonl", "", &opts).is_ok());
140        assert!(minify("python", "x = 1", &opts).is_err());
141    }
142
143    #[test]
144    fn supported_check() {
145        assert!(is_supported("json"));
146        assert!(is_supported("JSONL"));
147        assert!(is_supported("rust"));
148        assert!(is_supported("rs"));
149        assert!(is_supported("c++"));
150        assert!(is_supported("ts"));
151        assert!(!is_supported("python"));
152        assert!(!is_supported(""));
153    }
154
155    #[test]
156    fn refused_languages() {
157        assert!(refusal_reason("python").is_some());
158        assert!(refusal_reason("PY").is_some());
159        assert!(refusal_reason("yaml").is_some());
160        assert!(refusal_reason("yml").is_some());
161        assert!(refusal_reason("makefile").is_some());
162        assert!(refusal_reason("rust").is_none());
163        assert!(refusal_reason("json").is_none());
164    }
165}