Crafter.Build/interfaces/Crafter.Build-Lint.cppm

67 lines
3.3 KiB
Text
Raw Normal View History

2026-07-23 01:24:42 +02:00
// 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;
feat(lint): AST layer over libclang cursors Adds LintContext::Decls() — clang's view of the declarations written in the file — plus AddAstLintRule to register a rule that reads it. No rule uses it yet; the three that will are migrated separately. The declarations come back as a flat vector with parent indices rather than an opaque cursor handle: no lifetimes cross the project-DLL boundary, no callback hops back into project.so per node, and "is this at namespace scope or inside a function?" becomes an index lookup instead of a hand-rolled brace stack. Three things had to be solved for this to work at all on this codebase. libclang cannot see through `export`. A C++20 export declaration has no CXCursorKind, so `export namespace Crafter { … }` arrives as a childless CXCursor_UnexposedDecl and clang_visitChildren does not descend. Five of the eleven interfaces here are written that way — 677 lines, including Configuration and LintContext, yielding zero usable cursors. A plain `namespace` IS descended into, so the fix is to blank the keyword before parsing, byte-length preserving so every line and column still lands on the original file. `export module` is left alone or the unit stops being a module interface. Verified end-to-end against a fixture whose asserted line numbers match the unblanked file. PCMs are flag-locked, so each file has to parse with the flags that built it. CollectConfigSources now records which Configuration owns each source instead of flattening to a set, because three regimes coexist: the library, each test (carrying its own target, defines and -march), and project.cpp, which Build never touches and which therefore gets no command at all. libclang resolves its builtin headers relative to its own install path, which need not match the clang++ that wrote the PCMs. When it doesn't, every parse dies on "'stddef.h' file not found", so -resource-dir is passed explicitly from `clang++ -print-resource-dir`. Two flags on each declaration replace what would otherwise become more substring denylists: isExternC, and isForeignApi for a declaration that binds to an entity declared outside the project root — resolved through clang_getCursorReferenced and the same inside-the-root test the dependency walk already uses. Parameters and fields inherit it, so an exemption covers a whole signature rather than the one node that named the foreign entity. Failure is never silent. A fatal diagnostic leaves a fragment that is indistinguishable from a file declaring nothing, so it is reported as an error instead: the rule is skipped, a finding explains why, and summary.errors makes the run fail. --no-ast opts out deliberately and exits normally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:41 +02:00
// Skip rules registered with AddAstLintRule instead of building the
// module PCMs they need. The escape hatch for a fresh clone where the
// token-based rules are wanted immediately; the run then exits normally
// rather than erroring, so it must be asked for explicitly.
bool noAst = false;
2026-07-23 01:24:42 +02:00
// 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
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
// 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;
2026-07-23 01:24:42 +02:00
bool noRulesDefined = false; // project registered no rules at all
// Host-side only (like TestSummary::AllPassed), safe as in-class inline.
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
bool Clean() const { return findings.empty() && !noRulesDefined && errors == 0; }
2026-07-23 01:24:42 +02:00
};
// 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);
}