fix(lint): fail the files whose configuration has no compile command

When GetCompileCommand threw or the std module failed to build, the
lint driver cleared the configuration's command, and an empty command
means "not a translation unit" (project.cpp, a header): every rule that
needs an AST skipped the files quietly and the run passed them as clean.
MingwSysroot made that reachable for every mingw configuration whose
view cannot be made. The reason is now kept per configuration and
reported as an AST failure for each of its files, which fails the run
and prints it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 22:59:37 +01:00
commit 40dcf2271d
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB

View file

@ -1149,6 +1149,10 @@ format` applies it to disk, and `crafter-build lint` reports where it would.
// command per configuration — GetCompileCommand walks the dependency tree.
const bool anyRuleNeedsAst = std::ranges::any_of(rules, [](const LintRule* r) { return r->needsAst; });
std::unordered_map<const Configuration*, std::string> commands;
// Why a configuration of the build graph has no command: its files
// cannot be parsed, which fails the run. Cleared to nothing, the command
// read as "not a translation unit" and every semantic rule passed them.
std::unordered_map<const Configuration*, std::string> commandErrors;
if (anyRuleNeedsAst && !opts.noAst) {
for (Configuration* c : localConfigs) {
auto ensure = [&](const Configuration& cfg) {
@ -1166,10 +1170,14 @@ format` applies it to disk, and `crafter-build lint` reports where it would.
Progress::Task task(std::format("Building std PCM ({}-{})", cfg.target, cfg.march));
fs::create_directories(assembled.stdPcmDir);
std::string error = BuildStdPcm(cfg, assembled.stdPcmDir/"std.pcm");
if (!error.empty()) command.clear();
if (!error.empty()) {
command.clear();
commandErrors.emplace(&cfg, std::format("building the std module for {}: {}", cfg.target, error));
}
} catch (const std::exception&) {
command.clear(); // surfaced per file as an AST reason
}
} catch (const std::exception& e) {
command.clear();
commandErrors.emplace(&cfg, std::format("assembling the compile command for {}: {}", cfg.target, e.what()));
}
commands.emplace(&cfg, std::move(command));
};
@ -1200,6 +1208,11 @@ format` applies it to disk, and `crafter-build lint` reports where it would.
// decides what is safe to touch. Skip it and make the run fail.
if (rule->needsAst) {
if (opts.noAst) continue;
if (auto failed = commandErrors.find(owner); failed != commandErrors.end()) {
astFailures.emplace(file, failed->second);
skippedAstRules.insert(rule->name);
continue;
}
// No compile command means this file is not a translation unit
// of the build graph — project.cpp, which LoadProject compiles
// with its own flags, or a header. A semantic rule does not