feat(lint): naming reads the AST, deleting the scope heuristic

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>
This commit is contained in:
Jorijn van der Graaf 2026-07-30 23:53:26 +02:00
commit d55657b7ce
4 changed files with 227 additions and 255 deletions

View file

@ -50,44 +50,44 @@ namespace {
LibHandle handle = nullptr;
std::string error; // non-empty exactly when handle is null
decltype(&clang_createIndex) CreateIndex = nullptr;
decltype(&clang_disposeIndex) DisposeIndex = nullptr;
decltype(&clang_parseTranslationUnit) ParseTranslationUnit = nullptr;
decltype(&clang_disposeTranslationUnit) DisposeTranslationUnit = nullptr;
decltype(&clang_getFile) GetFile = nullptr;
decltype(&clang_getLocationForOffset) GetLocationForOffset = nullptr;
decltype(&clang_getRange) GetRange = nullptr;
decltype(&clang_getRangeStart) GetRangeStart = nullptr;
decltype(&clang_getRangeEnd) GetRangeEnd = nullptr;
decltype(&clang_getFileLocation) GetFileLocation = nullptr;
decltype(&clang_tokenize) Tokenize = nullptr;
decltype(&clang_disposeTokens) DisposeTokens = nullptr;
decltype(&clang_getTokenKind) GetTokenKind = nullptr;
decltype(&clang_getTokenExtent) GetTokenExtent = nullptr;
decltype(&clang_createIndex) createIndex = nullptr;
decltype(&clang_disposeIndex) disposeIndex = nullptr;
decltype(&clang_parseTranslationUnit) parseTranslationUnit = nullptr;
decltype(&clang_disposeTranslationUnit) disposeTranslationUnit = nullptr;
decltype(&clang_getFile) getFile = nullptr;
decltype(&clang_getLocationForOffset) getLocationForOffset = nullptr;
decltype(&clang_getRange) getRange = nullptr;
decltype(&clang_getRangeStart) getRangeStart = nullptr;
decltype(&clang_getRangeEnd) getRangeEnd = nullptr;
decltype(&clang_getFileLocation) getFileLocation = nullptr;
decltype(&clang_tokenize) tokenize = nullptr;
decltype(&clang_disposeTokens) disposeTokens = nullptr;
decltype(&clang_getTokenKind) getTokenKind = nullptr;
decltype(&clang_getTokenExtent) getTokenExtent = nullptr;
// AST layer.
decltype(&clang_getTranslationUnitCursor) GetTranslationUnitCursor = nullptr;
decltype(&clang_visitChildren) VisitChildren = nullptr;
decltype(&clang_getCursorKind) GetCursorKind = nullptr;
decltype(&clang_getCursorSpelling) GetCursorSpelling = nullptr;
decltype(&clang_getCursorType) GetCursorType = nullptr;
decltype(&clang_getTypeSpelling) GetTypeSpelling = nullptr;
decltype(&clang_getCursorLocation) GetCursorLocation = nullptr;
decltype(&clang_getCursorExtent) GetCursorExtent = nullptr;
decltype(&clang_getCursorReferenced) GetCursorReferenced = nullptr;
decltype(&clang_Cursor_getStorageClass) GetStorageClass = nullptr;
decltype(&clang_EnumDecl_isScoped) EnumDeclIsScoped = nullptr;
decltype(&clang_isCursorDefinition) IsCursorDefinition = nullptr;
decltype(&clang_Location_isFromMainFile) LocationIsFromMainFile = nullptr;
decltype(&clang_getNumDiagnostics) GetNumDiagnostics = nullptr;
decltype(&clang_getDiagnostic) GetDiagnostic = nullptr;
decltype(&clang_getDiagnosticSeverity) GetDiagnosticSeverity = nullptr;
decltype(&clang_getDiagnosticSpelling) GetDiagnosticSpelling = nullptr;
decltype(&clang_disposeDiagnostic) DisposeDiagnostic = nullptr;
decltype(&clang_getCString) GetCString = nullptr;
decltype(&clang_disposeString) DisposeString = nullptr;
decltype(&clang_getFileName) GetFileName = nullptr;
decltype(&clang_Cursor_isNull) CursorIsNull = nullptr;
decltype(&clang_getResultType) GetResultType = nullptr;
decltype(&clang_getTranslationUnitCursor) getTranslationUnitCursor = nullptr;
decltype(&clang_visitChildren) visitChildren = nullptr;
decltype(&clang_getCursorKind) getCursorKind = nullptr;
decltype(&clang_getCursorSpelling) getCursorSpelling = nullptr;
decltype(&clang_getCursorType) getCursorType = nullptr;
decltype(&clang_getTypeSpelling) getTypeSpelling = nullptr;
decltype(&clang_getCursorLocation) getCursorLocation = nullptr;
decltype(&clang_getCursorExtent) getCursorExtent = nullptr;
decltype(&clang_getCursorReferenced) getCursorReferenced = nullptr;
decltype(&clang_Cursor_getStorageClass) getStorageClass = nullptr;
decltype(&clang_EnumDecl_isScoped) enumDeclIsScoped = nullptr;
decltype(&clang_isCursorDefinition) isCursorDefinition = nullptr;
decltype(&clang_Location_isFromMainFile) locationIsFromMainFile = nullptr;
decltype(&clang_getNumDiagnostics) getNumDiagnostics = nullptr;
decltype(&clang_getDiagnostic) getDiagnostic = nullptr;
decltype(&clang_getDiagnosticSeverity) getDiagnosticSeverity = nullptr;
decltype(&clang_getDiagnosticSpelling) getDiagnosticSpelling = nullptr;
decltype(&clang_disposeDiagnostic) disposeDiagnostic = nullptr;
decltype(&clang_getCString) getCString = nullptr;
decltype(&clang_disposeString) disposeString = nullptr;
decltype(&clang_getFileName) getFileName = nullptr;
decltype(&clang_Cursor_isNull) cursorIsNull = nullptr;
decltype(&clang_getResultType) getResultType = nullptr;
};
LibClang LoadLibClang() {
@ -129,43 +129,43 @@ namespace {
slot = reinterpret_cast<std::remove_reference_t<decltype(slot)>>(LibrarySymbol(lib.handle, name));
if (!slot) missing.push_back(name);
};
bind(lib.CreateIndex, "clang_createIndex");
bind(lib.DisposeIndex, "clang_disposeIndex");
bind(lib.ParseTranslationUnit, "clang_parseTranslationUnit");
bind(lib.DisposeTranslationUnit, "clang_disposeTranslationUnit");
bind(lib.GetFile, "clang_getFile");
bind(lib.GetLocationForOffset, "clang_getLocationForOffset");
bind(lib.GetRange, "clang_getRange");
bind(lib.GetRangeStart, "clang_getRangeStart");
bind(lib.GetRangeEnd, "clang_getRangeEnd");
bind(lib.GetFileLocation, "clang_getFileLocation");
bind(lib.Tokenize, "clang_tokenize");
bind(lib.DisposeTokens, "clang_disposeTokens");
bind(lib.GetTokenKind, "clang_getTokenKind");
bind(lib.GetTokenExtent, "clang_getTokenExtent");
bind(lib.GetTranslationUnitCursor, "clang_getTranslationUnitCursor");
bind(lib.VisitChildren, "clang_visitChildren");
bind(lib.GetCursorKind, "clang_getCursorKind");
bind(lib.GetCursorSpelling, "clang_getCursorSpelling");
bind(lib.GetCursorType, "clang_getCursorType");
bind(lib.GetTypeSpelling, "clang_getTypeSpelling");
bind(lib.GetCursorLocation, "clang_getCursorLocation");
bind(lib.GetCursorExtent, "clang_getCursorExtent");
bind(lib.GetCursorReferenced, "clang_getCursorReferenced");
bind(lib.GetStorageClass, "clang_Cursor_getStorageClass");
bind(lib.EnumDeclIsScoped, "clang_EnumDecl_isScoped");
bind(lib.IsCursorDefinition, "clang_isCursorDefinition");
bind(lib.LocationIsFromMainFile, "clang_Location_isFromMainFile");
bind(lib.GetNumDiagnostics, "clang_getNumDiagnostics");
bind(lib.GetDiagnostic, "clang_getDiagnostic");
bind(lib.GetDiagnosticSeverity, "clang_getDiagnosticSeverity");
bind(lib.GetDiagnosticSpelling, "clang_getDiagnosticSpelling");
bind(lib.DisposeDiagnostic, "clang_disposeDiagnostic");
bind(lib.GetCString, "clang_getCString");
bind(lib.DisposeString, "clang_disposeString");
bind(lib.GetFileName, "clang_getFileName");
bind(lib.CursorIsNull, "clang_Cursor_isNull");
bind(lib.GetResultType, "clang_getResultType");
bind(lib.createIndex, "clang_createIndex");
bind(lib.disposeIndex, "clang_disposeIndex");
bind(lib.parseTranslationUnit, "clang_parseTranslationUnit");
bind(lib.disposeTranslationUnit, "clang_disposeTranslationUnit");
bind(lib.getFile, "clang_getFile");
bind(lib.getLocationForOffset, "clang_getLocationForOffset");
bind(lib.getRange, "clang_getRange");
bind(lib.getRangeStart, "clang_getRangeStart");
bind(lib.getRangeEnd, "clang_getRangeEnd");
bind(lib.getFileLocation, "clang_getFileLocation");
bind(lib.tokenize, "clang_tokenize");
bind(lib.disposeTokens, "clang_disposeTokens");
bind(lib.getTokenKind, "clang_getTokenKind");
bind(lib.getTokenExtent, "clang_getTokenExtent");
bind(lib.getTranslationUnitCursor, "clang_getTranslationUnitCursor");
bind(lib.visitChildren, "clang_visitChildren");
bind(lib.getCursorKind, "clang_getCursorKind");
bind(lib.getCursorSpelling, "clang_getCursorSpelling");
bind(lib.getCursorType, "clang_getCursorType");
bind(lib.getTypeSpelling, "clang_getTypeSpelling");
bind(lib.getCursorLocation, "clang_getCursorLocation");
bind(lib.getCursorExtent, "clang_getCursorExtent");
bind(lib.getCursorReferenced, "clang_getCursorReferenced");
bind(lib.getStorageClass, "clang_Cursor_getStorageClass");
bind(lib.enumDeclIsScoped, "clang_EnumDecl_isScoped");
bind(lib.isCursorDefinition, "clang_isCursorDefinition");
bind(lib.locationIsFromMainFile, "clang_Location_isFromMainFile");
bind(lib.getNumDiagnostics, "clang_getNumDiagnostics");
bind(lib.getDiagnostic, "clang_getDiagnostic");
bind(lib.getDiagnosticSeverity, "clang_getDiagnosticSeverity");
bind(lib.getDiagnosticSpelling, "clang_getDiagnosticSpelling");
bind(lib.disposeDiagnostic, "clang_disposeDiagnostic");
bind(lib.getCString, "clang_getCString");
bind(lib.disposeString, "clang_disposeString");
bind(lib.getFileName, "clang_getFileName");
bind(lib.cursorIsNull, "clang_Cursor_isNull");
bind(lib.getResultType, "clang_getResultType");
if (!missing.empty()) {
lib.handle = nullptr;
lib.error = std::format("loaded {} but it is missing {}", candidates.front(), join(missing));
@ -229,36 +229,36 @@ namespace {
unsaved.Contents = content.data();
unsaved.Length = static_cast<std::uint32_t>(content.size());
CXIndex index = lc.CreateIndex(0, 0);
CXIndex index = lc.createIndex(0, 0);
if (!index) return {};
CXTranslationUnit tu = lc.ParseTranslationUnit(index, path.c_str(), args.data(), static_cast<std::int32_t>(args.size()), &unsaved, 1, CXTranslationUnit_SingleFileParse | CXTranslationUnit_SkipFunctionBodies | CXTranslationUnit_KeepGoing);
CXTranslationUnit tu = lc.parseTranslationUnit(index, path.c_str(), args.data(), static_cast<std::int32_t>(args.size()), &unsaved, 1, CXTranslationUnit_SingleFileParse | CXTranslationUnit_SkipFunctionBodies | CXTranslationUnit_KeepGoing);
if (!tu) {
lc.DisposeIndex(index);
lc.disposeIndex(index);
return {};
}
std::vector<LintToken> tokens;
if (CXFile cxFile = lc.GetFile(tu, path.c_str())) {
CXSourceRange whole = lc.GetRange(lc.GetLocationForOffset(tu, cxFile, 0), lc.GetLocationForOffset(tu, cxFile, static_cast<std::uint32_t>(content.size())));
if (CXFile cxFile = lc.getFile(tu, path.c_str())) {
CXSourceRange whole = lc.getRange(lc.getLocationForOffset(tu, cxFile, 0), lc.getLocationForOffset(tu, cxFile, static_cast<std::uint32_t>(content.size())));
CXToken* raw = nullptr;
std::uint32_t count = 0;
lc.Tokenize(tu, whole, &raw, &count);
lc.tokenize(tu, whole, &raw, &count);
tokens.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
CXSourceRange extent = lc.GetTokenExtent(tu, raw[i]);
CXSourceRange extent = lc.getTokenExtent(tu, raw[i]);
std::uint32_t line = 0;
std::uint32_t column = 0;
std::uint32_t begin = 0;
std::uint32_t end = 0;
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, &line, &column, &begin);
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &end);
lc.getFileLocation(lc.getRangeStart(extent), nullptr, &line, &column, &begin);
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &end);
if (end < begin || begin > content.size()) continue;
tokens.push_back({MapTokenKind(lc.GetTokenKind(raw[i])), begin, std::min<std::size_t>(end - begin, content.size() - begin), line, column});
tokens.push_back({MapTokenKind(lc.getTokenKind(raw[i])), begin, std::min<std::size_t>(end - begin, content.size() - begin), line, column});
}
if (raw) lc.DisposeTokens(tu, raw, count);
if (raw) lc.disposeTokens(tu, raw, count);
}
lc.DisposeTranslationUnit(tu);
lc.DisposeIndex(index);
lc.disposeTranslationUnit(tu);
lc.disposeIndex(index);
return tokens;
}
@ -269,9 +269,9 @@ namespace {
// works this out for itself now: the initialiser resolves to a
// declaration in clang-c/, outside the project, so the declaration is
// flagged as foreign API and exempt. No suppression comment needed.
const char* raw = lc.GetCString(s);
const char* raw = lc.getCString(s);
std::string out = raw ? raw : "";
lc.DisposeString(s);
lc.disposeString(s);
return out;
}
@ -386,17 +386,17 @@ namespace {
// from where the declaration actually lives.
bool ResolvesOutsideProject(const LibClang& lc, CXCursor cursor, const fs::path& projectRoot) {
if (projectRoot.empty()) return false;
CXCursor target = lc.GetCursorReferenced(cursor);
if (lc.CursorIsNull(target)) return false;
CXSourceLocation location = lc.GetCursorLocation(target);
if (lc.LocationIsFromMainFile(location)) return false;
CXCursor target = lc.getCursorReferenced(cursor);
if (lc.cursorIsNull(target)) return false;
CXSourceLocation location = lc.getCursorLocation(target);
if (lc.locationIsFromMainFile(location)) return false;
CXFile file = nullptr;
std::uint32_t line = 0;
std::uint32_t column = 0;
std::uint32_t offset = 0;
lc.GetFileLocation(location, &file, &line, &column, &offset);
lc.getFileLocation(location, &file, &line, &column, &offset);
if (!file) return false;
std::string path = TakeString(lc, lc.GetFileName(file));
std::string path = TakeString(lc, lc.getFileName(file));
if (path.empty()) return false;
return !PathInsideRoot(fs::path(path), projectRoot);
}
@ -404,13 +404,13 @@ namespace {
CXChildVisitResult VisitDecl(CXCursor cursor, CXCursor, CXClientData data) {
DeclWalk& walk = *static_cast<DeclWalk*>(data);
const LibClang& lc = *walk.lc;
CXSourceLocation location = lc.GetCursorLocation(cursor);
CXSourceLocation location = lc.getCursorLocation(cursor);
// Every declaration the imported modules bring in arrives here too —
// an unfiltered visit of one interface unit walks ~495,000 cursors from
// std alone. Prune before doing any work.
if (!lc.LocationIsFromMainFile(location)) return CXChildVisit_Continue;
if (!lc.locationIsFromMainFile(location)) return CXChildVisit_Continue;
CXCursorKind kind = lc.GetCursorKind(cursor);
CXCursorKind kind = lc.getCursorKind(cursor);
LintDeclKind mapped = MapCursorKind(kind);
if (mapped == LintDeclKind::Other) {
// A foreign reference inside a VARIABLE, FIELD or PARAMETER is an
@ -430,51 +430,51 @@ namespace {
// enclosing-declaration stack stays accurate: the callback is never
// told when a subtree ends.
if (kind == CXCursor_LinkageSpec) {
CXSourceRange extent = lc.GetCursorExtent(cursor);
CXSourceRange extent = lc.getCursorExtent(cursor);
std::uint32_t specBegin = 0;
std::uint32_t specEnd = 0;
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, nullptr, nullptr, &specBegin);
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &specEnd);
lc.getFileLocation(lc.getRangeStart(extent), nullptr, nullptr, nullptr, &specBegin);
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &specEnd);
std::string_view text;
if (specBegin < walk.content->size()) {
text = std::string_view(walk.content->data() + specBegin, std::min<std::size_t>(specEnd - specBegin, walk.content->size() - specBegin));
}
bool isC = IsExternCLinkage(text);
if (isC) ++walk.externCDepth;
lc.VisitChildren(cursor, &VisitDecl, &walk);
lc.visitChildren(cursor, &VisitDecl, &walk);
if (isC) --walk.externCDepth;
return CXChildVisit_Continue;
}
lc.VisitChildren(cursor, &VisitDecl, &walk);
lc.visitChildren(cursor, &VisitDecl, &walk);
return CXChildVisit_Continue;
}
LintDecl decl;
decl.kind = mapped;
decl.name = TakeString(lc, lc.GetCursorSpelling(cursor));
decl.name = TakeString(lc, lc.getCursorSpelling(cursor));
// For anything callable, `type` is the RESULT type rather than the
// whole function type: the parameters arrive as their own Parameter
// declarations, so spelling them here too would make every rule
// reading `type` report each one twice.
bool callable = mapped == LintDeclKind::Function || mapped == LintDeclKind::Method;
decl.type = TakeString(lc, lc.GetTypeSpelling(callable ? lc.GetResultType(lc.GetCursorType(cursor)) : lc.GetCursorType(cursor)));
decl.type = TakeString(lc, lc.getTypeSpelling(callable ? lc.getResultType(lc.getCursorType(cursor)) : lc.getCursorType(cursor)));
CXFile nameFile = nullptr;
std::uint32_t nameLine = 0;
std::uint32_t nameColumn = 0;
std::uint32_t nameOffset = 0;
lc.GetFileLocation(location, &nameFile, &nameLine, &nameColumn, &nameOffset);
lc.getFileLocation(location, &nameFile, &nameLine, &nameColumn, &nameOffset);
decl.line = nameLine;
decl.column = nameColumn;
CXSourceRange extent = lc.GetCursorExtent(cursor);
CXSourceRange extent = lc.getCursorExtent(cursor);
std::uint32_t begin = 0;
std::uint32_t end = 0;
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, nullptr, nullptr, &begin);
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &end);
lc.getFileLocation(lc.getRangeStart(extent), nullptr, nullptr, nullptr, &begin);
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &end);
decl.begin = begin;
decl.end = std::min<std::size_t>(end, walk.content->size());
decl.isDefinition = lc.IsCursorDefinition(cursor) != 0;
decl.isStatic = lc.GetStorageClass(cursor) == CX_SC_Static;
decl.isScopedEnum = mapped == LintDeclKind::Enum && lc.EnumDeclIsScoped(cursor) != 0;
decl.isDefinition = lc.isCursorDefinition(cursor) != 0;
decl.isStatic = lc.getStorageClass(cursor) == CX_SC_Static;
decl.isScopedEnum = mapped == LintDeclKind::Enum && lc.enumDeclIsScoped(cursor) != 0;
// Inside an extern "C" block, where a C API's spelling is not ours to
// modernise. NOT clang_getCursorLanguage: its default answer for a
// plain function, variable or parameter is CXLanguage_C even in a C++
@ -492,7 +492,7 @@ namespace {
walk.out->push_back(std::move(decl));
walk.stack.push_back(walk.out->size() - 1);
lc.VisitChildren(cursor, &VisitDecl, &walk);
lc.visitChildren(cursor, &VisitDecl, &walk);
walk.stack.pop_back();
return CXChildVisit_Continue;
}
@ -504,7 +504,7 @@ namespace {
walk.content = &content;
walk.projectRoot = &projectRoot;
walk.out = &decls;
lc.VisitChildren(lc.GetTranslationUnitCursor(tu), &VisitDecl, &walk);
lc.visitChildren(lc.getTranslationUnitCursor(tu), &VisitDecl, &walk);
// A declaration on an interop boundary makes its parameters and fields
// interop too — the exemption has to cover the whole signature, not
// just the node that happened to name the foreign entity.
@ -579,34 +579,34 @@ namespace {
unsaved.Contents = buffer.data();
unsaved.Length = static_cast<std::uint32_t>(buffer.size());
CXIndex index = lc.CreateIndex(0, 0);
CXIndex index = lc.createIndex(0, 0);
if (!index) {
result.error = "clang_createIndex failed";
return result;
}
CXTranslationUnit tu = lc.ParseTranslationUnit(index, path.c_str(), argv.data(), static_cast<std::int32_t>(argv.size()), &unsaved, 1, CXTranslationUnit_None);
CXTranslationUnit tu = lc.parseTranslationUnit(index, path.c_str(), argv.data(), static_cast<std::int32_t>(argv.size()), &unsaved, 1, CXTranslationUnit_None);
if (!tu) {
lc.DisposeIndex(index);
lc.disposeIndex(index);
result.error = "clang could not create a translation unit";
return result;
}
std::string fatal;
std::uint32_t diagnostics = lc.GetNumDiagnostics(tu);
std::uint32_t diagnostics = lc.getNumDiagnostics(tu);
for (std::uint32_t i = 0; i < diagnostics && fatal.empty(); ++i) {
CXDiagnostic diagnostic = lc.GetDiagnostic(tu, i);
if (lc.GetDiagnosticSeverity(diagnostic) == CXDiagnostic_Fatal) {
fatal = TakeString(lc, lc.GetDiagnosticSpelling(diagnostic));
CXDiagnostic diagnostic = lc.getDiagnostic(tu, i);
if (lc.getDiagnosticSeverity(diagnostic) == CXDiagnostic_Fatal) {
fatal = TakeString(lc, lc.getDiagnosticSpelling(diagnostic));
}
lc.DisposeDiagnostic(diagnostic);
lc.disposeDiagnostic(diagnostic);
}
if (fatal.empty()) {
result.decls = WalkDecls(lc, tu, buffer, projectRoot);
} else {
result.error = std::move(fatal);
}
lc.DisposeTranslationUnit(tu);
lc.DisposeIndex(index);
lc.disposeTranslationUnit(tu);
lc.disposeIndex(index);
return result;
}

View file

@ -71,12 +71,12 @@ namespace Crafter {
std::string src = contents.str();
std::string pathStr = path.string();
const char* file_name_list[1] = { pathStr.c_str() };
const char* fileNameList[1] = { pathStr.c_str() };
const char* shaderSource = src.data();
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
glslang::TShader shader(glslangType);
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, file_name_list, 1);
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1);
shader.setEntryPoint(entrypoint.c_str());
shader.setSourceEntryPoint(entrypoint.c_str());
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);

View file

@ -82,15 +82,6 @@ inline bool IsCharPointer(std::string_view type) {
return base == "char";
}
// The identifier ending right before position `pos` (exclusive), or empty.
inline std::string_view WordBefore(std::string_view s, std::size_t pos) {
std::size_t end = pos;
while (end > 0 && (s[end - 1] == ' ' || s[end - 1] == '\t')) --end;
std::size_t begin = end;
while (begin > 0 && (IsWordChar(s[begin - 1]) || s[begin - 1] == '~')) --begin;
return s.substr(begin, end - begin);
}
inline void AddProjectLintRules(Crafter::Configuration& cfg) {
using Crafter::LintContext;
@ -118,132 +109,77 @@ inline void AddProjectLintRules(Crafter::Configuration& cfg) {
}
});
// Naming: functions/types PascalCase, variables camelCase, statics and
// namespace-scope globals PascalCase. A line-based scope tracker decides
// whether a declaration sits at namespace scope (global) or inside a
// function/type (local/member). Heuristic by nature — report-only.
cfg.AddLintRule("naming", [](LintContext& ctx) {
// Naming: types and functions PascalCase, variables camelCase, with
// statics, namespace-scope globals and constexpr constants PascalCase.
//
// Reads the AST. The previous version needed four std::regex, a hand-rolled
// {/} scope stack, a cumulative paren-depth counter to avoid mistaking a
// wrapped parameter list for a declaration, a keyword denylist, and a
// "function-shaped line" guess whose own comment conceded it was heuristic.
// All of it existed to answer two questions clang answers directly: what
// kind of declaration is this, and what encloses it.
cfg.AddAstLintRule("naming", [](LintContext& ctx) {
if (!IsCppFile(ctx)) return;
std::vector<std::string_view> lines = Lines(ctx.CommentStripped());
enum class Scope { Namespace, Type, Function, Other };
std::vector<Scope> stack;
auto currentScope = [&]() { return stack.empty() ? Scope::Namespace : stack.back(); };
static const std::regex typeDecl(R"(\b(?:class|struct|union)\s+(?:CRAFTER_API\s+)?([A-Za-z_]\w*))");
static const std::regex enumDecl(R"(\benum\s+(?:class\s+|struct\s+)?([A-Za-z_]\w*))");
static const std::regex usingDecl(R"(^\s*using\s+([A-Za-z_]\w*)\s*=)");
static const std::regex varDecl(
R"(^\s*((?:static|constexpr|const|inline|mutable|thread_local|export|CRAFTER_API)\s+)*)"
R"((?:std::)?[A-Za-z_][\w:]*(?:<[^;={]*>)?(?:\s*[&*])*\s+([A-Za-z_]\w*)\s*(=|;|\{))");
static const std::unordered_set<std::string_view> keywords = {
"if", "for", "while", "switch", "catch", "return", "else", "do", "case", "goto",
"new", "delete", "throw", "using", "namespace", "template", "typedef", "friend",
"public", "private", "protected", "class", "struct", "enum", "union", "import",
"module", "export", "break", "continue", "co_return", "co_await", "co_yield",
"sizeof", "alignof", "decltype", "static_assert", "operator", "try", "requires",
};
// Cumulative paren depth at the start of each line: declaration and
// function checks only run at depth 0, so wrapped parameter lists and
// continuation lines (`) {` closers) never look like declarations.
std::int64_t parenDepth = 0;
for (std::size_t i = 0; i < lines.size(); ++i) {
std::string_view trimmed = Trim(lines[i]);
std::string lineStr(lines[i]);
std::smatch m;
bool atDepth0 = parenDepth == 0;
parenDepth = std::max<std::int64_t>(0, parenDepth + ParenDelta(lines[i]));
if (!trimmed.starts_with('#') && atDepth0) {
// Type / alias names must be PascalCase.
if (std::regex_search(lineStr, m, typeDecl) || std::regex_search(lineStr, m, enumDecl)) {
std::string name = m[1].str();
if (!keywords.contains(name) && !IsPascalCase(name)) {
ctx.Report(i + 1, std::format("type '{}' should be PascalCase", name));
using Kind = Crafter::LintDeclKind;
std::span<const Crafter::LintDecl> decls = ctx.Decls();
for (const Crafter::LintDecl& decl : decls) {
if (decl.name.empty()) continue; // anonymous namespace, unnamed struct
// A declaration with C language linkage is named by the C library
// it mirrors — `extern "C" int setenv(...)` is not ours to rename.
if (decl.isExternC) continue;
// Namespace scope is now a lookup rather than a brace count, and it
// is exact for a local declared inside a function body.
bool atNamespaceScope = decl.parent == Crafter::LintNoParent
|| decls[decl.parent].kind == Kind::Namespace;
switch (decl.kind) {
case Kind::Class:
case Kind::Struct:
case Kind::Union:
case Kind::Enum:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type '{}' should be PascalCase", decl.name));
}
}
if (std::regex_search(lineStr, m, usingDecl) && !IsPascalCase(m[1].str())) {
ctx.Report(i + 1, std::format("type alias '{}' should be PascalCase", m[1].str()));
}
// Function definitions: identifier before the first '(' on a
// line that ends where a body opens or closes — a multi-line
// def's trailing '{' or a one-liner's trailing '}'. Statement
// calls with inline lambda arguments look similar
// (`bool x = std::any_of(..., [](T v) { ... });`) but end in
// ';' and/or carry '=' before the name — both excluded.
// Lambdas ("](") and control keywords are skipped; ctors/
// dtors pass the Pascal check by construction; `main` and
// operators exempt.
std::size_t bodyBrace = trimmed.find('{');
bool functionShaped = trimmed.ends_with('{') || trimmed.ends_with('}');
if (functionShaped && currentScope() != Scope::Function && !trimmed.starts_with("return")) {
std::size_t paren = trimmed.find('(');
if (paren != std::string_view::npos && paren > 0 && paren < bodyBrace) {
std::string_view name = WordBefore(trimmed, paren);
char before = trimmed[paren - 1];
bool looksLikeDef = !name.empty() && IsWordChar(before)
&& !keywords.contains(name) && name != "main"
&& !name.starts_with('~');
// Require a type token before the name (or a
// qualified Class::Name), with no '=' in front —
// plain calls and initializations don't match.
if (looksLikeDef) {
std::size_t nameStart = trimmed.rfind(name, paren);
std::string_view prefix = Trim(trimmed.substr(0, nameStart));
looksLikeDef = !prefix.empty() && !prefix.contains('=')
&& (IsWordChar(prefix.back()) || prefix.back() == '>'
|| prefix.back() == '*' || prefix.back() == '&'
|| prefix.ends_with("::"));
break;
case Kind::TypeAlias:
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("type alias '{}' should be PascalCase", decl.name));
}
break;
case Kind::Function:
case Kind::Method:
// main is spelled by the language; operators by their
// symbol. Constructors and destructors take their type's
// name and are separate kinds, so they never arrive here.
if (decl.name == "main" || decl.name.starts_with("operator")) break;
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("function '{}' should be PascalCase", decl.name));
}
break;
case Kind::Variable:
// constexpr variables are compile-time constants and take
// constant naming, like statics and globals.
if (decl.isStatic || decl.isConstexpr || atNamespaceScope) {
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("{} '{}' should be PascalCase",
decl.isStatic ? "static variable"
: decl.isConstexpr ? "constexpr constant"
: "global variable", decl.name));
}
if (looksLikeDef && !IsPascalCase(name)) {
ctx.Report(i + 1, std::format("function '{}' should be PascalCase", name));
} else if (!IsCamelCase(decl.name)) {
ctx.Report(decl.line, std::format("variable '{}' should be camelCase", decl.name));
}
break;
case Kind::Field:
if (decl.isStatic || decl.isConstexpr) {
if (!IsPascalCase(decl.name)) {
ctx.Report(decl.line, std::format("static member '{}' should be PascalCase", decl.name));
}
} else if (!IsCamelCase(decl.name)) {
ctx.Report(decl.line, std::format("member '{}' should be camelCase", decl.name));
}
}
// Variable declarations: camelCase locally, PascalCase for
// statics and namespace-scope globals.
if (std::regex_search(lineStr, m, varDecl)) {
std::string name = m[2].str();
std::string_view typeToken = Trim(std::string_view(lineStr).substr(0, static_cast<std::size_t>(m.position(2))));
std::string_view firstWord = typeToken.substr(0, typeToken.find_first_of(" \t<"));
if (!keywords.contains(firstWord) && !keywords.contains(name)) {
bool isStatic = lineStr.contains("static ");
// constexpr variables are compile-time constants —
// constant naming (PascalCase) like statics/globals.
bool isConstexpr = lineStr.contains("constexpr ");
bool global = currentScope() == Scope::Namespace;
if ((isStatic || global || isConstexpr) && !IsPascalCase(name)) {
ctx.Report(i + 1, std::format("{} '{}' should be PascalCase",
isStatic ? "static variable"
: isConstexpr ? "constexpr constant"
: "global variable", name));
} else if (!isStatic && !global && !isConstexpr && !IsCamelCase(name)) {
ctx.Report(i + 1, std::format("variable '{}' should be camelCase", name));
}
}
}
}
// Scope tracking: classify each '{' opened on this line; pop on '}'.
for (std::size_t c = 0; c < lines[i].size(); ++c) {
if (lines[i][c] == '{') {
Scope kind = Scope::Other;
std::string_view upTo = Trim(lines[i].substr(0, c));
if (trimmed.starts_with("namespace") || upTo.contains("namespace ")) {
kind = Scope::Namespace;
} else if (std::regex_search(lineStr, typeDecl) || std::regex_search(lineStr, enumDecl)) {
kind = Scope::Type;
} else if (upTo.ends_with(')') || upTo.ends_with("const") || upTo.ends_with("noexcept")
|| upTo.ends_with("->") || trimmed.starts_with("extern")) {
kind = Scope::Function; // function/lambda/control body — all non-global
}
stack.push_back(kind);
} else if (lines[i][c] == '}') {
if (!stack.empty()) stack.pop_back();
}
break;
default:
break;
}
}
});

View file

@ -327,6 +327,42 @@ int main() {
Check(HasFinding(r.summary, "'raw'"), "no-char-pointer: a deduced char* local is still ours");
}
// Cases the line-based heuristic structurally could not see. It only looked
// at declarations starting at cumulative paren depth 0 and inferred scope
// from a running {/} count, so a wrapped signature, a specifier split over
// two lines, or a local shadowing a member all escaped it.
{
RuleRun r = RunRule("#include <string>\n"
"namespace Outer {\n"
" int wrapped_function(\n"
" int first,\n"
" int second) { return first + second; }\n"
" static\n"
" int splitStatic = 1;\n"
" struct Holder {\n"
" int Member = 0;\n"
" void Method() {\n"
" int Local = 1;\n"
" (void)Local;\n"
" }\n"
" };\n"
"}\n"
"extern \"C\" int c_api_entry(const char* name);\n",
"naming", LintMode::Report);
// A signature wrapped over lines is still a function declaration.
Check(HasFinding(r.summary, "'wrapped_function' should be PascalCase"), "naming: wrapped signature is seen");
// `static` on its own line still applies to the declaration below it.
Check(HasFinding(r.summary, "'splitStatic' should be PascalCase"), "naming: split specifier is seen");
// Members are camelCase; the enclosing kind decides, not a brace count.
Check(HasFinding(r.summary, "'Member' should be camelCase"), "naming: member is checked as a member");
// A local inside a method is a local, not a member and not a global.
Check(HasFinding(r.summary, "'Local' should be camelCase"), "naming: local inside a method is a local");
// Named by libc, not by us.
Check(!HasFinding(r.summary, "c_api_entry"), "naming: extern \"C\" declaration is exempt");
Check(!HasFinding(r.summary, "'Method'"), "naming: PascalCase method passes");
Check(!HasFinding(r.summary, "'Holder'"), "naming: PascalCase type passes");
}
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;