pmat 3.30.1

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
// Dead code output formatting - included from dead_code_handlers.rs
// NO `use` imports or `#!` inner attributes allowed here.

/// Format dead code result based on output format
fn format_dead_code_result(
    result: &crate::models::dead_code::DeadCodeResult,
    format: &DeadCodeOutputFormat,
    scope: DeadCodeReportScope,
) -> Result<String> {
    match format {
        DeadCodeOutputFormat::Json => format_dead_code_as_json(result),
        DeadCodeOutputFormat::Sarif => format_dead_code_as_sarif(result),
        DeadCodeOutputFormat::Summary => format_dead_code_as_summary_scoped(result, scope),
        DeadCodeOutputFormat::Markdown => format_dead_code_as_markdown(result),
    }
}

/// Format result as JSON
fn format_dead_code_as_json(result: &crate::models::dead_code::DeadCodeResult) -> Result<String> {
    Ok(serde_json::to_string_pretty(result)?)
}

/// Format result as SARIF
fn format_dead_code_as_sarif(result: &crate::models::dead_code::DeadCodeResult) -> Result<String> {
    use crate::models::dead_code::{ConfidenceLevel, DeadCodeType};
    use serde_json::json;

    let sarif = json!({
        "version": "2.1.0",
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "pmat",
                    "version": env!("CARGO_PKG_VERSION"),
                    "informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
                    "rules": [{
                        "id": "dead-code",
                        "name": "Dead Code Detection",
                        "shortDescription": {
                            "text": "Code that is never executed or referenced"
                        },
                        "fullDescription": {
                            "text": "Detects functions, classes, and code blocks that are not reachable from any entry point"
                        },
                        "defaultConfiguration": {
                            "level": "warning"
                        }
                    }]
                }
            },
            "results": result.files.iter().flat_map(|file| {
                file.items.iter().map(|item| {
                    let level = match file.confidence {
                        ConfidenceLevel::High => "error",
                        ConfidenceLevel::Medium => "warning",
                        ConfidenceLevel::Low => "note",
                    };
                    json!({
                        "ruleId": "dead-code",
                        "level": level,
                        "message": {
                            "text": format!("{}: {}",
                                match item.item_type {
                                    DeadCodeType::Function => "Dead function",
                                    DeadCodeType::Class => "Dead class",
                                    DeadCodeType::Variable => "Dead variable",
                                    DeadCodeType::Module => "Dead module",
                                    DeadCodeType::UnreachableCode => "Unreachable code",
                                    DeadCodeType::Other => "Dead item",
                                },
                                item.reason
                            )
                        },
                        "locations": [{
                            "physicalLocation": {
                                "artifactLocation": {
                                    "uri": &file.path
                                },
                                "region": {
                                    "startLine": item.line
                                }
                            }
                        }]
                    })
                }).collect::<Vec<_>>()
            }).collect::<Vec<_>>()
        }]
    });
    Ok(serde_json::to_string_pretty(&sarif)?)
}

/// Format result as summary, with no analyzer scope information.
///
/// Every figure the renderer cannot verify is then reported as unknown rather
/// than guessed: no project-wide percentage, no name for the skipped files.
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn format_dead_code_as_summary(
    result: &crate::models::dead_code::DeadCodeResult,
) -> Result<String> {
    format_dead_code_as_summary_scoped(result, DeadCodeReportScope::default())
}

/// Format result as summary, told what the analyzer could and could not measure.
fn format_dead_code_as_summary_scoped(
    result: &crate::models::dead_code::DeadCodeResult,
    scope: DeadCodeReportScope,
) -> Result<String> {
    let mut output = String::new();

    write_dead_code_header(&mut output, result, scope)?;

    // Print the breakdown whenever there is anything to break down. Gating on
    // `dead_functions > 0` hid it exactly when it was needed: a report of 26
    // dead lines made entirely of dead fields showed no types at all.
    if result.summary.total_dead_lines > 0 || !result.files.is_empty() {
        write_dead_code_by_type_section(&mut output, result)?;
    }

    if !result.files.is_empty() {
        write_top_files_section(&mut output, &result.files)?;
    }

    Ok(output)
}

/// Write dead code analysis header section
fn write_dead_code_header(
    output: &mut String,
    result: &crate::models::dead_code::DeadCodeResult,
    scope: DeadCodeReportScope,
) -> Result<()> {
    use crate::cli::colors as c;
    use std::fmt::Write;

    writeln!(output, "{}\n", c::header("Dead Code Analysis Summary"))?;
    writeln!(
        output,
        "  {} {}",
        c::label("Files analyzed:"),
        c::number(&result.analyzed_files.to_string())
    )?;
    // Name the narrowing. Without it, a repo whose only dead code lives in test
    // code got an all-zero report headed by the project's whole file count -- a
    // clean bill of health for files the scan never opened.
    //
    // The parenthetical comes from the analyzer that did the skipping. It used
    // to be one hardcoded phrase naming the test tree AND the example and
    // benchmark trees, neither of which the cargo scan skips -- and the Top
    // Files list directly beneath it was made of `examples/`.
    if result.total_files > result.analyzed_files {
        writeln!(
            output,
            "  {} {} ({})",
            c::label("Files skipped (out of scope):"),
            c::number(&(result.total_files - result.analyzed_files).to_string()),
            scope
                .skipped_kind
                .unwrap_or("the analyzer did not say which files")
        )?;
    }
    writeln!(
        output,
        "  {} {}",
        c::label("Files with dead code:"),
        c::number(&result.summary.files_with_dead_code.to_string())
    )?;
    // Name the cap instead of letting the reported count stand for the total:
    // `files_with_dead_code: 26` used to head a list of 4.
    // `--include`/`--exclude` filter the REPORT, not the walk. Saying so is the
    // difference between "Files analyzed: 2" being a fact about the scan and it
    // reading as a claim that the filter narrowed the scan -- which it does not:
    // `--include 'examples/**'` on a two-file crate still analyzes both files
    // and still divides by the whole project's lines.
    if scope.list_filtered {
        writeln!(
            output,
            "  {} --include/--exclude filter this report, not the scan; \
             the counts above and the percentage below cover every scanned file",
            c::label("Note:")
        )?;
    }
    let omitted = result.files_omitted();
    if omitted > 0 {
        // Name every cut that could have removed them. A file dropped by
        // `--exclude 'src/**'` was reported as "below --min-dead-lines",
        // blaming a threshold that had nothing to do with it.
        let mut reasons: Vec<&str> = vec!["below --min-dead-lines"];
        if result.files_truncated {
            reasons.push("beyond --top-files");
        }
        if scope.list_filtered {
            reasons.push("removed by --include/--exclude");
        }
        writeln!(
            output,
            "  {} {} ({} not listed: {})",
            c::label("Files found with dead code:"),
            c::number(&result.files_with_dead_code_found.to_string()),
            c::number(&omitted.to_string()),
            reasons.join(" or ")
        )?;
    }
    writeln!(
        output,
        "  {} {}",
        c::label("Total dead lines:"),
        c::number(&result.summary.total_dead_lines.to_string())
    )?;
    // Named for its scope. Both this and the figure `--fail-on-violation`
    // compares are real, but they measure different sets — this one covers the
    // files actually LISTED (which `--min-dead-lines` and `--top-files` shrink),
    // the gate's covers every line walked. Printing this one as plain "Dead code
    // percentage" made them look like one number disagreeing with itself: a run
    // could report 0.0% here while the gate failed the same run at 100%.
    //
    // When there is NO project-wide figure at all — the multi-language analyzer
    // never counts total project lines — the note says so. It used to print a
    // bare "Dead code percentage: 100.0%" and then, in the same run,
    // `--fail-on-violation` bailed with "no project-wide dead-code percentage
    // was measured for this project": the report both stated a measurement and
    // denied making one.
    let scope_note = match scope.project_dead_percentage {
        Some(project) if result.files_omitted() > 0 || scope.list_filtered => {
            format!(" (listed files only; project-wide: {project:.1}%)")
        }
        Some(_) => String::new(),
        None => {
            " (listed files only; no project-wide figure was measured for this project)".to_string()
        }
    };
    writeln!(
        output,
        "  {}{} {}\n",
        c::label("Dead code percentage:"),
        c::dim(&scope_note),
        c::pct(f64::from(result.summary.dead_percentage), 5.0, 15.0)
    )?;

    Ok(())
}

/// Write dead code by type breakdown section.
///
/// Every reported dead item lands in exactly one row. The "Dead variables" row
/// used to print `summary.dead_modules` — a module count under a variable label
/// — and fields, constants and statics were counted in no row at all, so a
/// report of 26 dead lines could show four zeros beneath it.
fn write_dead_code_by_type_section(
    output: &mut String,
    result: &crate::models::dead_code::DeadCodeResult,
) -> Result<()> {
    use crate::cli::colors as c;
    use std::fmt::Write;

    let summary = &result.summary;
    let other_items = count_other_items(result);

    writeln!(output, "{}\n", c::subheader("Dead Code by Type"))?;
    for (label, value) in [
        ("Dead functions:", summary.dead_functions),
        ("Dead classes:", summary.dead_classes),
        ("Dead modules:", summary.dead_modules),
        ("Other (fields, constants, statics):", other_items),
        ("Unreachable blocks:", summary.unreachable_blocks),
    ] {
        writeln!(
            output,
            "  {} {}",
            c::label(label),
            c::number(&value.to_string())
        )?;
    }

    Ok(())
}

/// The items that belong in the "Other" row: bindings (fields, constants,
/// statics, variants) and anything whose kind the producer could not name.
///
/// ONE implementation, called by both renderers. #928: this predicate used to
/// be written out twice as `matches!(item.item_type, DeadCodeType::Variable)`,
/// and because a dead MODULE was also typed `Variable` back then, every module
/// was counted in BOTH the "Dead modules" row and this one — the two rows
/// summed to more items than the report listed.
fn count_other_items(result: &crate::models::dead_code::DeadCodeResult) -> usize {
    use crate::models::dead_code::DeadCodeType;

    result
        .files
        .iter()
        .flat_map(|f| f.items.iter())
        .filter(|item| matches!(item.item_type, DeadCodeType::Variable | DeadCodeType::Other))
        .count()
}

/// Write top files with dead code section
fn write_top_files_section(
    output: &mut String,
    files: &[crate::models::dead_code::FileDeadCodeMetrics],
) -> Result<()> {
    use crate::cli::colors as c;
    use std::fmt::Write;

    writeln!(output, "\n{}\n", c::subheader("Top Files with Dead Code"))?;
    for (i, file) in files.iter().take(10).enumerate() {
        writeln!(
            output,
            "  {}. {} - {} dead ({} lines)",
            c::number(&(i + 1).to_string()),
            c::path(&file.path),
            c::pct(f64::from(file.dead_percentage), 5.0, 15.0),
            c::number(&file.dead_lines.to_string())
        )?;
    }

    Ok(())
}

/// Format result as markdown
fn format_dead_code_as_markdown(
    result: &crate::models::dead_code::DeadCodeResult,
) -> Result<String> {
    let mut sections = Vec::new();

    // Build summary section
    sections.push(format_dead_code_summary_section(result));

    // Build breakdown section if needed
    if result.summary.dead_functions > 0 {
        sections.push(format_dead_code_breakdown_section(result));
    }

    // Build file details section if needed
    if !result.files.is_empty() {
        sections.push(format_dead_code_file_details_section(&result.files));
    }

    // Build recommendations section
    sections.push(format_dead_code_recommendations_section());

    Ok(sections.join("\n"))
}

fn format_dead_code_summary_section(result: &crate::models::dead_code::DeadCodeResult) -> String {
    format!(
        "# Dead Code Analysis Report\n\n\
         ## Summary\n\n\
         | Metric | Value |\n\
         |--------|-------|\n\
         | Files Analyzed | {} |\n\
         | Files Skipped (out of scope) | {} |\n\
         | Files with Dead Code | {} |\n\
         | Total Dead Lines | {} |\n\
         | Dead Code Percentage | {:.2}% |\n",
        result.analyzed_files,
        result.total_files.saturating_sub(result.analyzed_files),
        result.summary.files_with_dead_code,
        result.summary.total_dead_lines,
        result.summary.dead_percentage
    )
}

/// Write the markdown breakdown table.
///
/// The `Modules` row prints `summary.dead_modules`, which is a MODULE count on
/// the cargo path. It was labelled `Variables` here -- the same mislabel the
/// text renderer carried (#721) -- so a cargo run reported its dead modules
/// under a row heading no producer fills. Fields, constants and statics are
/// counted from the items themselves, exactly as the text renderer does, so
/// every reported dead item lands in one row.
fn format_dead_code_breakdown_section(result: &crate::models::dead_code::DeadCodeResult) -> String {
    let summary = &result.summary;
    let other_items = count_other_items(result);

    format!(
        "## Dead Code Breakdown\n\n\
         | Type | Count |\n\
         |------|-------|\n\
         | Functions | {} |\n\
         | Classes | {} |\n\
         | Modules | {} |\n\
         | Other (fields, constants, statics) | {} |\n\
         | Unreachable Blocks | {} |\n",
        summary.dead_functions,
        summary.dead_classes,
        summary.dead_modules,
        other_items,
        summary.unreachable_blocks
    )
}

fn format_dead_code_file_details_section(
    files: &[crate::models::dead_code::FileDeadCodeMetrics],
) -> String {
    let mut output = String::from(
        "## File Details\n\n\
         | File | Dead % | Dead Lines | Confidence | Items |\n\
         |------|--------|------------|------------|-------|\n",
    );

    for file in files.iter().take(20) {
        output.push_str(&format!(
            "| {} | {:.1}% | {} | {:?} | {} |\n",
            file.path,
            file.dead_percentage,
            file.dead_lines,
            file.confidence,
            file.items.len()
        ));
    }

    output
}

fn format_dead_code_recommendations_section() -> String {
    "## Recommendations\n\n\
     1. **Review High Confidence Dead Code**: Start with files marked as high confidence.\n\
     2. **Check Test Coverage**: Dead code often indicates missing tests.\n\
     3. **Consider Refactoring**: Large amounts of dead code may indicate design issues.\n\
     4. **Remove Carefully**: Ensure code is truly dead before removal.\n"
        .to_string()
}

/// Write dead code output to file or stdout
async fn write_dead_code_output(content: String, output: Option<PathBuf>) -> Result<()> {
    match output {
        Some(path) => {
            tokio::fs::write(&path, content).await?;
            crate::status_eprintln!("📝 Results written to: {}", path.display());
        }
        None => {
            println!("{content}");
        }
    }
    Ok(())
}