feat(lint): no-char-pointer reads the AST, retiring the interop denylist
The rule was `\bchar\s*\*` over the text minus a substring denylist — argv, getenv, setenv, dlerror, c_str, .data(, reinterpret_cast, extern ". Every entry was a patch for one interop site, the list could only grow as libraries arrived, and each entry disabled the rule for the whole LINE it appeared on. It now walks declarations and asks the question the denylist was approximating: whose header dictates this spelling? A declaration with C language linkage, or one whose initialiser binds to an entity declared outside the project root, is somebody else's API and keeps its spelling. getenv, c_str and friends are exempt because of where they are declared, not because they are named here, so a new external library needs no new entry. Two bugs found while testing this, both of which had made the rule silently pass over the entire repository: clang_getCursorLanguage cannot be used to detect extern "C". Its default answer is CXLanguage_C for a plain function, variable or parameter even in a C++ translation unit, so isExternC was true almost everywhere and exempted everything. Replaced by tracking CXCursor_LinkageSpec depth during the walk, reading the extent text to tell extern "C" from extern "C++". Attributing any foreign reference in a subtree to the enclosing declaration was too broad: a function that merely touched libc++ somewhere in its body would exempt its own signature. Narrowed to initialiser contexts — a variable, field or parameter — which is where a binding to a foreign API actually occurs. Also: functions now carry their RESULT type rather than the whole function type, since the parameters arrive as their own declarations and would otherwise be reported twice. main's parameters are exempt structurally, its signature being fixed by the language rather than chosen here. Two sites keep an explicit lint-disable, both Crafter::Run taking main's argv verbatim. That is two visible, reasoned suppressions in place of a denylist that silently disabled the rule for every line mentioning one of eight tokens. ExternalCloneDir and ExternalIncludeFlags are now exposed from :External, so a source that includes an external dependency's headers can be parsed without running a build to discover where they are. BuildExternal derives its own working directory through the same function, so the two cannot drift. Crafter.Build-Shader.cpp needed this to parse at all. A file with no compile command — project.cpp, which LoadProject builds with its own flags — is not a translation unit of the build graph, so AST rules skip it the way a rule self-filters by extension. That is distinct from a file that should have parsed and did not, which stays an error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29888fc5ba
commit
91e4f59a94
7 changed files with 209 additions and 40 deletions
|
|
@ -295,6 +295,38 @@ int main() {
|
|||
Check(r.text == source, "fixed-width-types leaves type names inside a raw string alone");
|
||||
}
|
||||
|
||||
// no-char-pointer reads the AST, so it distinguishes OUR char* from one
|
||||
// whose spelling belongs to somebody else's header. This is what replaced
|
||||
// the substring denylist (argv, getenv, c_str, reinterpret_cast, …): each
|
||||
// entry there disabled the rule for a whole line, and the list could only
|
||||
// grow as new libraries arrived.
|
||||
{
|
||||
RuleRun r = RunRule("#include <cstdlib>\n"
|
||||
"#include <string>\n"
|
||||
"extern \"C\" const char* CApiEntry(const char* path);\n"
|
||||
"char* OurBadApi(char* input) { return input; }\n"
|
||||
"void F() {\n"
|
||||
" char* fromLibc = std::getenv(\"HOME\");\n"
|
||||
" std::string mine = \"ok\";\n"
|
||||
" const char* toLibc = mine.c_str();\n"
|
||||
" auto raw = reinterpret_cast<char*>(&mine);\n"
|
||||
"}\n",
|
||||
"no-char-pointer", LintMode::Report);
|
||||
// Ours, so reported: the declaration and its parameter.
|
||||
Check(HasFinding(r.summary, "'OurBadApi'"), "no-char-pointer: our own char* return is reported");
|
||||
Check(HasFinding(r.summary, "'input'"), "no-char-pointer: our own char* parameter is reported");
|
||||
// Foreign, so exempt — each for a reason, not by name.
|
||||
Check(!HasFinding(r.summary, "'CApiEntry'"), "no-char-pointer: extern \"C\" declaration is exempt");
|
||||
Check(!HasFinding(r.summary, "'path'"), "no-char-pointer: extern \"C\" parameter is exempt");
|
||||
Check(!HasFinding(r.summary, "'fromLibc'"), "no-char-pointer: a value from libc is exempt");
|
||||
Check(!HasFinding(r.summary, "'toLibc'"), "no-char-pointer: a value from c_str() is exempt");
|
||||
// A local whose deduced type is char* is still our declaration, so it
|
||||
// is reported. The old denylist exempted every line mentioning
|
||||
// reinterpret_cast; a deliberate low-level cast now takes an explicit
|
||||
// lint-disable comment, which is at least visible at the site.
|
||||
Check(HasFinding(r.summary, "'raw'"), "no-char-pointer: a deduced char* local is still ours");
|
||||
}
|
||||
|
||||
if (Failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", Failures);
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue