Crafter.Build/interfaces/Crafter.Build-Lint.cppm
Jorijn van der Graaf 5888af29ed feat(lint): libclang-backed token layer
Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the
substrate the rules will move onto. Nothing consumes it yet.

libclang is dlopen'd rather than linked: -lclang would break the mingw and
MSVC cross-builds at link time and would put a libclang.so.NN runtime
dependency into the otherwise self-contained release tarballs. The clang-c
header is used for its declarations only, and the function-pointer table is
typed with decltype so the signatures cannot drift from the real API.

Three properties this buys that the hand-rolled scanners could not have:

  - a raw string literal or block comment is ONE token, so the documented
    "raw string literals are not recognized" limitation goes away;
  - `//` inside a literal is not a comment, so LineHasComment() replaces the
    Line(n).contains("//") probes that false-positive on it;
  - tokens cover preprocessor branches that are inactive for the host, since
    clang_tokenize lexes rather than evaluates #if. Token rules therefore
    keep seeing every platform's code, which an AST could not offer.

The parse backing the tokenizer is expected to fail on module units — no
PCMs, no build flags — and that is fine, because lexing has no semantic
prerequisites. Verified in the new tests.

LintSummary::Clean() now counts `errors`. It previously ignored them, so an
infrastructure failure that produced no findings reported clean and exited
0; a missing libclang would have been exactly that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00

62 lines
3 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Lint;
import std;
import :Clang;
export namespace Crafter {
enum class LintMode {
// `lint`: diagnose only; transform output becomes would-reformat
// findings. Never writes. The default.
Report,
// `format --check`: dry run; record the files that would change.
Check,
// `format`: rewrite changed files in place.
Apply,
};
struct RunLintOptions {
// Rule-name globs ('*', '?'); empty = every registered rule.
std::vector<std::string> globs;
// Enumerate matching rule names without running them.
bool listOnly = false;
LintMode mode = LintMode::Report;
// Absolute path of the loaded project.cpp. It is linted too, and its
// parent directory is the project root that decides which dependency
// Configurations contribute rules/files (GitProject / cache-dir deps
// are foreign code and skipped). Empty => fall back to cfg.path.
std::filesystem::path projectFile;
};
struct LintSummary {
std::vector<LintFinding> findings; // sorted by (file, line); filled in every mode
// Apply: files rewritten on disk. Report/Check: files a transform
// would change. The `format` verb's exit code keys off this in Check
// mode; formatting files in Apply mode is success.
std::vector<std::filesystem::path> changedFiles;
std::size_t filesLinted = 0;
std::size_t rulesRun = 0; // rules remaining after glob filter
// Rule exceptions, write failures, and infrastructure failures such as
// libclang not loading. Counted in Clean() so a run that could not do
// its job never looks like a run that found nothing.
std::size_t errors = 0;
bool noRulesDefined = false; // project registered no rules at all
// Host-side only (like TestSummary::AllPassed), safe as in-class inline.
bool Clean() const { return findings.empty() && !noRulesDefined && errors == 0; }
};
// Run the project's lint rules over its own sources: module interfaces
// (+ partitions), implementations, cFiles, cuda, shaders, declared tests'
// sources, and project.cpp itself — for the root Configuration plus every
// transitive dependency whose path lies inside the project root. Rules
// run in registration order per file, each seeing the previous rule's
// transform output; a rule that throws is reverted and surfaced as a
// finding + error. Only Apply mode writes to disk, and only files whose
// final content differs from the original. Prints per-mode output:
// findings compiler-style (Report), would-change paths (Check), or
// formatted paths (Apply), plus a summary line.
CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
}