Crafter.Graphics has to ship each of its compute shaders in two builds
of one source: one that indexes VK_EXT_descriptor_heap and one that
reads the same slots from a descriptor set, for drivers without the
extension. A Shader was a path, an entry point and a stage, and its
output name was derived from the path alone, so one source could only
be compiled once.
Shader gains `defines`, spelled like the compiler's -D ("NAME" is
defined to 1, "NAME=VALUE" to VALUE) and handed to glslang as a
preamble, and `variant`, which names the output: "ui.comp.glsl" with
variant "sets" becomes "ui.comp.sets.spv". OutputName() is the one
place that name is formed; Check, Compile and the forwarding of a
dependency's shaders into the consumer's bin dir all use it.
Two fields make one mistake easy: defines set, variant forgotten. Both
entries then compiled in parallel into the same file, the build
reported success with whichever finished last, and Check() called both
up to date from then on. The same has always been true of a.vert next
to a.frag. Build() now refuses a configuration in which two entries
share an output name and says which.
A define without a name, or one spanning lines, and a variant with a
path separator are rejected by name. Left to glslang the first two
surface as "-1:<line>" with no file, and the last as a failed open
after the whole compile.
Not addressed: Check() still compares only the source's mtime, so a
change to the defines, or to an included file, does not recompile
(#35).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first cut scanned the initialiser's tokens and required every one to be a
literal or an operator. That is exactly the kind of approximation this work has
been removing, and it was wrong in both directions:
const int A = sizeof(Big); missed — `sizeof` is a keyword
const int B = Base + 1; missed — `Base` is an identifier
const auto C = 5_notConstexpr; would have been reported, wrongly
clang_Cursor_Evaluate answers the question directly. Evaluating a variable
declaration evaluates its initialiser, so anything that folds is recognised and
a call result still is not. Costs nothing measurable — lint stays at ~15s.
Found one real case the token version could not see: an EShMessages fold over
three glslang enum constants in Crafter.Build-Shader.cpp. Promoting it to
constexpr then made `naming` ask for constant naming, since a constexpr
variable is a compile-time constant — so it is `Messages` now. Two rules
agreeing on the same declaration is the intended behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two rules the AST makes possible, plus the mutation analysis behind them.
const-local reports a local that is never written. It is restricted to SCALARS
— integers, bools, enums, floating types — and that restriction is what makes
the answer exact rather than a guess: a scalar has no member functions, so the
only ways to write one are assignment, ++/--, having its address taken, or
binding to a non-const reference. All four are now tracked in the walk:
- assignment and compound assignment visit their LEFT operand in a write
context, the right one normally;
- ++/-- and & write their operand;
- a call argument is checked against the callee's parameter type, so passing
to `const int&` or by value is a read while `int&` is a write;
- initialising a non-const reference writes what it binds to.
For a class type a non-const method call could mutate it, and deciding that is
the whole-program analysis clang-tidy does, so those are simply out of scope
rather than guessed at.
constexpr-constant promotes a const constant whose initialiser is made only of
literals and operators, so `const int A = 1 << 4;` qualifies and
`const int B = Compute();` does not.
On this repository const-local found 103 candidates, which was too many to be
useful, and the reason was informative: most were range-for bindings and
pointer locals. `for (T* const x : …)` and `T* const p` are not spellings
anybody writes, and the useful constness for a pointer is on the pointee, which
this rule cannot advise on. Excluding both leaves 36, all plain bool or enum
locals worth fixing — isWasm, isPe, exists, writes, isC and so on. Those 36 are
fixed in this commit; the compiler verified every one.
Both rules are report-only. The analysis is exact, but adding const is a
judgement about intent as much as mechanics, and a wrong suggestion should cost
a glance rather than a build. const-local also deliberately does not become a
transform: inserting `const` before a shared type would apply it to every
declarator in a multi-declarator statement, including any that IS written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule was 125 lines: four std::regex, a hand-rolled {/} scope stack, a
cumulative paren-depth counter so a wrapped parameter list would not look like
a declaration, a 40-entry keyword denylist, and a "function-shaped line" guess
whose own comment conceded it was heuristic. Storage class came from
lineStr.contains("static "). It is now ~55 lines that ask clang what kind of
declaration each thing is and what encloses it.
The three regressions the old version carried special cases for — a call with
an inline lambda argument, a bare statement call, a one-liner method — need no
handling at all, because a call is not a declaration. Their tests pass
unchanged.
On this repository the exact version found 42 violations the heuristic had
never been able to see, all real:
- 37 members of the libclang function-pointer table added two commits ago
were PascalCase. The old varDecl regex could not match a declaration whose
type is decltype(&f), so they were silently skipped. Renamed to camelCase,
which mirrors clang_createIndex -> createIndex more closely anyway.
- Crafter.Build-Shader.cpp had a snake_case local, file_name_list, invisible
to the heuristic for the same reason (it declares a const char* array).
- Four extern "C" declarations of libc functions in tests were reported as
badly-named functions. Those are named by the C library, so C language
linkage is now an exemption — the same principled test no-char-pointer
uses, rather than another denylist entry.
New tests cover what the line-based version structurally could not reach: a
signature wrapped over several lines, `static` on its own line above the
declaration it applies to, and a member versus a local inside a method.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Canonical LGPL-3.0-only text, GPL-3.0 companion, SPDX headers on all
first-party sources, MIT for examples/. Vendored lib/ untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>