Adds the FixIt collector + per-file unified-diff patch writer behind a new `fold scan --fix-output <path>` flag. Patch-only -- no in-place rewrites -- so a bad suggestion can't silently overwrite source. Consumers (only when the transform is provably sound): - UninitLocalRead: scalar integer locals only. `int x;` -> `int x = 0;`. Non-scalar types deferred. - DeprecatedApi: regex extracts `[Uu]se foo()` from the deprecation message; silent skip when unparseable rather than hallucinating a rewrite. Patches include 3 lines of context above + below each fix so `git apply --check` accepts them. End-to-end verified against a synthetic fixture.
112 lines
4.1 KiB
C++
112 lines
4.1 KiB
C++
// Matrix row C9 — confirm the patch writer emits a unified diff
|
|
// that `git apply` can consume.
|
|
//
|
|
// Test pins:
|
|
// 1. A FixIt that replaces a single line emits a hunk with one
|
|
// `-` and one `+` line, valid `@@` header.
|
|
// 2. FixIts on disjoint files produce one hunk per file in the
|
|
// same patch.
|
|
// 3. FixIts ordered out-of-line get sorted by start_line.
|
|
// 4. Missing source file produces no hunk (no abort).
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include "fold/report/finding.hpp"
|
|
#include "fold/report/patch_writer.hpp"
|
|
|
|
namespace {
|
|
|
|
std::filesystem::path writeTempFile(const std::string& name, const std::string& contents) {
|
|
auto p = std::filesystem::temp_directory_path() / name;
|
|
std::ofstream out(p);
|
|
out << contents;
|
|
out.close();
|
|
return p;
|
|
}
|
|
|
|
fold::Finding makeFinding(std::string file, int line, int col, std::string replacement,
|
|
std::string desc) {
|
|
fold::Finding f;
|
|
f.kind = fold::FindingKind::UninitLocalRead;
|
|
fold::FixIt fix;
|
|
fix.file = std::move(file);
|
|
fix.start_line = line;
|
|
fix.start_column = col;
|
|
fix.end_line = line;
|
|
fix.end_column = col;
|
|
fix.replacement = std::move(replacement);
|
|
fix.description = std::move(desc);
|
|
f.fix_its.push_back(std::move(fix));
|
|
return f;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("patch_writer: single-line in-place insertion emits a valid hunk",
|
|
"[c9][patch_writer][single]") {
|
|
auto file = writeTempFile("c9_single.c", "int x;\n");
|
|
fold::Finding f =
|
|
makeFinding(file.filename().string(), 1, 6, " = 0", "Initialise uninitialised local");
|
|
f.fix_its[0].end_column = 6; // pure insertion before `;`
|
|
|
|
auto patch =
|
|
fold::report::writeFixItPatch({f}, std::filesystem::temp_directory_path().string());
|
|
REQUIRE(patch.find("--- a/c9_single.c") != std::string::npos);
|
|
REQUIRE(patch.find("+++ b/c9_single.c") != std::string::npos);
|
|
REQUIRE(patch.find("@@") != std::string::npos);
|
|
REQUIRE(patch.find("-int x;") != std::string::npos);
|
|
REQUIRE(patch.find("+int x = 0;") != std::string::npos);
|
|
|
|
std::filesystem::remove(file);
|
|
}
|
|
|
|
TEST_CASE("patch_writer: FixIts on two files produce two hunks", "[c9][patch_writer][multi-file]") {
|
|
auto a = writeTempFile("c9_a.c", "int a;\n");
|
|
auto b = writeTempFile("c9_b.c", "int b;\n");
|
|
|
|
fold::Finding fa = makeFinding(a.filename().string(), 1, 6, " = 0", "init a");
|
|
fa.fix_its[0].end_column = 6;
|
|
fold::Finding fb = makeFinding(b.filename().string(), 1, 6, " = 0", "init b");
|
|
fb.fix_its[0].end_column = 6;
|
|
|
|
auto patch =
|
|
fold::report::writeFixItPatch({fa, fb}, std::filesystem::temp_directory_path().string());
|
|
REQUIRE(patch.find("--- a/c9_a.c") != std::string::npos);
|
|
REQUIRE(patch.find("--- a/c9_b.c") != std::string::npos);
|
|
|
|
std::filesystem::remove(a);
|
|
std::filesystem::remove(b);
|
|
}
|
|
|
|
TEST_CASE("patch_writer: FixIts within one file are sorted by line", "[c9][patch_writer][sort]") {
|
|
auto file = writeTempFile("c9_sort.c", "int a;\nint b;\nint c;\n");
|
|
|
|
// Emit in reverse order — writer must sort.
|
|
fold::Finding f3 = makeFinding(file.filename().string(), 3, 6, " = 0", "init c");
|
|
f3.fix_its[0].end_column = 6;
|
|
fold::Finding f1 = makeFinding(file.filename().string(), 1, 6, " = 0", "init a");
|
|
f1.fix_its[0].end_column = 6;
|
|
|
|
auto patch =
|
|
fold::report::writeFixItPatch({f3, f1}, std::filesystem::temp_directory_path().string());
|
|
auto line1_pos = patch.find("-int a;");
|
|
auto line3_pos = patch.find("-int c;");
|
|
REQUIRE(line1_pos != std::string::npos);
|
|
REQUIRE(line3_pos != std::string::npos);
|
|
REQUIRE(line1_pos < line3_pos);
|
|
|
|
std::filesystem::remove(file);
|
|
}
|
|
|
|
TEST_CASE("patch_writer: missing source file is skipped, not fatal",
|
|
"[c9][patch_writer][missing]") {
|
|
fold::Finding f = makeFinding("nonexistent_c9_file.c", 1, 1, "X", "noop");
|
|
auto patch =
|
|
fold::report::writeFixItPatch({f}, std::filesystem::temp_directory_path().string());
|
|
// Either an empty string or one with no hunk for the missing path.
|
|
REQUIRE(patch.find("--- a/nonexistent_c9_file.c") == std::string::npos);
|
|
}
|