feat(lint): fixed-width-types keeps widths that a foreign API chose
The rewrite itself stays token-shaped — it edits type SPELLINGS, which an AST
discards — but what it must not touch now comes from the AST.
The old exemption was per LINE: `int main`, `argc`, `argv`, `extern "`. Being a
transform, a missed exemption here does not over-report, it emits code that no
longer matches the API being called, so this is the rule where guessing from
substrings mattered most. And being per-line, it also disabled the rule for
anything sharing a line with one of those words.
Now a declaration with C language linkage, or one whose initialiser binds to an
entity declared outside the project, contributes a protected byte range and
keeps its spelling. That answers the case directly: a function declared in
somebody else's header taking `unsigned int` keeps `unsigned int`, and a local
initialised from strtoul keeps `unsigned long`, because of where those are
declared rather than because of what the line says.
main is protected from the start of its declaration to the opening brace of its
body, not for its whole extent. Its signature is fixed by the language; its body
is ordinary code. Three findings on this repository came out of that
distinction, all correct:
for (int i = 1; i < argc; ++i) -> for (std::int32_t i = 1; ...)
skipped before only because `argc` appeared on the line, plus Crafter::Run's own
`int argc` and return type, which are ours rather than the language's.
All three AST rules now share one interop test instead of carrying a denylist
each, and it is the same test: whose header dictates this spelling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d55657b7ce
commit
6438cb9ebb
4 changed files with 68 additions and 10 deletions
|
|
@ -363,6 +363,38 @@ int main() {
|
|||
Check(!HasFinding(r.summary, "'Holder'"), "naming: PascalCase type passes");
|
||||
}
|
||||
|
||||
// fixed-width-types must not rewrite an integer whose width somebody else's
|
||||
// header chose — the rewrite would leave code that no longer matches the API
|
||||
// it calls. Derived from the AST, so the exemption is per-declaration rather
|
||||
// than the old per-line textual one, which both missed cases and disabled
|
||||
// the rule for anything else sharing a line with `argc`/`extern "`.
|
||||
{
|
||||
RuleRun r = RunRule("#include <cstdlib>\n"
|
||||
"extern \"C\" unsigned long CApiCall(unsigned int flags);\n"
|
||||
"long OurOwnApi(int value) { return value; }\n"
|
||||
"void F() {\n"
|
||||
" unsigned long fromLibc = std::strtoul(\"1\", nullptr, 10);\n"
|
||||
" unsigned ours = 1;\n"
|
||||
" (void)fromLibc; (void)ours;\n"
|
||||
"}\n"
|
||||
"int main(int argc, char** argv) {\n"
|
||||
" for (int i = 0; i < argc; ++i) { (void)argv[i]; }\n"
|
||||
" return 0;\n"
|
||||
"}\n",
|
||||
"fixed-width-types", LintMode::Apply);
|
||||
// Somebody else's widths, kept.
|
||||
Check(r.text.contains("extern \"C\" unsigned long CApiCall(unsigned int flags);"), "fixed-width: extern \"C\" signature keeps its widths");
|
||||
Check(r.text.contains("unsigned long fromLibc = std::strtoul"), "fixed-width: a value from a C library keeps its width");
|
||||
Check(r.text.contains("int main(int argc, char** argv)"), "fixed-width: main's signature is untouched");
|
||||
// Ours, converted.
|
||||
Check(r.text.contains("std::int64_t OurOwnApi(std::int32_t value)"), "fixed-width: our own signature converts");
|
||||
Check(r.text.contains("std::uint32_t ours = 1;"), "fixed-width: our own local converts");
|
||||
// The loop counter inside main's BODY is ordinary code. The old rule
|
||||
// skipped it because `argc` appeared on the same line; only the
|
||||
// signature is exempt now, not everything near it.
|
||||
Check(r.text.contains("for (std::int32_t i = 0;"), "fixed-width: main's body is not exempt, only its signature");
|
||||
}
|
||||
|
||||
if (Failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", Failures);
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue