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:
parent
91e4f59a94
commit
d55657b7ce
4 changed files with 227 additions and 255 deletions
|
|
@ -50,44 +50,44 @@ namespace {
|
||||||
LibHandle handle = nullptr;
|
LibHandle handle = nullptr;
|
||||||
std::string error; // non-empty exactly when handle is null
|
std::string error; // non-empty exactly when handle is null
|
||||||
|
|
||||||
decltype(&clang_createIndex) CreateIndex = nullptr;
|
decltype(&clang_createIndex) createIndex = nullptr;
|
||||||
decltype(&clang_disposeIndex) DisposeIndex = nullptr;
|
decltype(&clang_disposeIndex) disposeIndex = nullptr;
|
||||||
decltype(&clang_parseTranslationUnit) ParseTranslationUnit = nullptr;
|
decltype(&clang_parseTranslationUnit) parseTranslationUnit = nullptr;
|
||||||
decltype(&clang_disposeTranslationUnit) DisposeTranslationUnit = nullptr;
|
decltype(&clang_disposeTranslationUnit) disposeTranslationUnit = nullptr;
|
||||||
decltype(&clang_getFile) GetFile = nullptr;
|
decltype(&clang_getFile) getFile = nullptr;
|
||||||
decltype(&clang_getLocationForOffset) GetLocationForOffset = nullptr;
|
decltype(&clang_getLocationForOffset) getLocationForOffset = nullptr;
|
||||||
decltype(&clang_getRange) GetRange = nullptr;
|
decltype(&clang_getRange) getRange = nullptr;
|
||||||
decltype(&clang_getRangeStart) GetRangeStart = nullptr;
|
decltype(&clang_getRangeStart) getRangeStart = nullptr;
|
||||||
decltype(&clang_getRangeEnd) GetRangeEnd = nullptr;
|
decltype(&clang_getRangeEnd) getRangeEnd = nullptr;
|
||||||
decltype(&clang_getFileLocation) GetFileLocation = nullptr;
|
decltype(&clang_getFileLocation) getFileLocation = nullptr;
|
||||||
decltype(&clang_tokenize) Tokenize = nullptr;
|
decltype(&clang_tokenize) tokenize = nullptr;
|
||||||
decltype(&clang_disposeTokens) DisposeTokens = nullptr;
|
decltype(&clang_disposeTokens) disposeTokens = nullptr;
|
||||||
decltype(&clang_getTokenKind) GetTokenKind = nullptr;
|
decltype(&clang_getTokenKind) getTokenKind = nullptr;
|
||||||
decltype(&clang_getTokenExtent) GetTokenExtent = nullptr;
|
decltype(&clang_getTokenExtent) getTokenExtent = nullptr;
|
||||||
// AST layer.
|
// AST layer.
|
||||||
decltype(&clang_getTranslationUnitCursor) GetTranslationUnitCursor = nullptr;
|
decltype(&clang_getTranslationUnitCursor) getTranslationUnitCursor = nullptr;
|
||||||
decltype(&clang_visitChildren) VisitChildren = nullptr;
|
decltype(&clang_visitChildren) visitChildren = nullptr;
|
||||||
decltype(&clang_getCursorKind) GetCursorKind = nullptr;
|
decltype(&clang_getCursorKind) getCursorKind = nullptr;
|
||||||
decltype(&clang_getCursorSpelling) GetCursorSpelling = nullptr;
|
decltype(&clang_getCursorSpelling) getCursorSpelling = nullptr;
|
||||||
decltype(&clang_getCursorType) GetCursorType = nullptr;
|
decltype(&clang_getCursorType) getCursorType = nullptr;
|
||||||
decltype(&clang_getTypeSpelling) GetTypeSpelling = nullptr;
|
decltype(&clang_getTypeSpelling) getTypeSpelling = nullptr;
|
||||||
decltype(&clang_getCursorLocation) GetCursorLocation = nullptr;
|
decltype(&clang_getCursorLocation) getCursorLocation = nullptr;
|
||||||
decltype(&clang_getCursorExtent) GetCursorExtent = nullptr;
|
decltype(&clang_getCursorExtent) getCursorExtent = nullptr;
|
||||||
decltype(&clang_getCursorReferenced) GetCursorReferenced = nullptr;
|
decltype(&clang_getCursorReferenced) getCursorReferenced = nullptr;
|
||||||
decltype(&clang_Cursor_getStorageClass) GetStorageClass = nullptr;
|
decltype(&clang_Cursor_getStorageClass) getStorageClass = nullptr;
|
||||||
decltype(&clang_EnumDecl_isScoped) EnumDeclIsScoped = nullptr;
|
decltype(&clang_EnumDecl_isScoped) enumDeclIsScoped = nullptr;
|
||||||
decltype(&clang_isCursorDefinition) IsCursorDefinition = nullptr;
|
decltype(&clang_isCursorDefinition) isCursorDefinition = nullptr;
|
||||||
decltype(&clang_Location_isFromMainFile) LocationIsFromMainFile = nullptr;
|
decltype(&clang_Location_isFromMainFile) locationIsFromMainFile = nullptr;
|
||||||
decltype(&clang_getNumDiagnostics) GetNumDiagnostics = nullptr;
|
decltype(&clang_getNumDiagnostics) getNumDiagnostics = nullptr;
|
||||||
decltype(&clang_getDiagnostic) GetDiagnostic = nullptr;
|
decltype(&clang_getDiagnostic) getDiagnostic = nullptr;
|
||||||
decltype(&clang_getDiagnosticSeverity) GetDiagnosticSeverity = nullptr;
|
decltype(&clang_getDiagnosticSeverity) getDiagnosticSeverity = nullptr;
|
||||||
decltype(&clang_getDiagnosticSpelling) GetDiagnosticSpelling = nullptr;
|
decltype(&clang_getDiagnosticSpelling) getDiagnosticSpelling = nullptr;
|
||||||
decltype(&clang_disposeDiagnostic) DisposeDiagnostic = nullptr;
|
decltype(&clang_disposeDiagnostic) disposeDiagnostic = nullptr;
|
||||||
decltype(&clang_getCString) GetCString = nullptr;
|
decltype(&clang_getCString) getCString = nullptr;
|
||||||
decltype(&clang_disposeString) DisposeString = nullptr;
|
decltype(&clang_disposeString) disposeString = nullptr;
|
||||||
decltype(&clang_getFileName) GetFileName = nullptr;
|
decltype(&clang_getFileName) getFileName = nullptr;
|
||||||
decltype(&clang_Cursor_isNull) CursorIsNull = nullptr;
|
decltype(&clang_Cursor_isNull) cursorIsNull = nullptr;
|
||||||
decltype(&clang_getResultType) GetResultType = nullptr;
|
decltype(&clang_getResultType) getResultType = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
LibClang LoadLibClang() {
|
LibClang LoadLibClang() {
|
||||||
|
|
@ -129,43 +129,43 @@ namespace {
|
||||||
slot = reinterpret_cast<std::remove_reference_t<decltype(slot)>>(LibrarySymbol(lib.handle, name));
|
slot = reinterpret_cast<std::remove_reference_t<decltype(slot)>>(LibrarySymbol(lib.handle, name));
|
||||||
if (!slot) missing.push_back(name);
|
if (!slot) missing.push_back(name);
|
||||||
};
|
};
|
||||||
bind(lib.CreateIndex, "clang_createIndex");
|
bind(lib.createIndex, "clang_createIndex");
|
||||||
bind(lib.DisposeIndex, "clang_disposeIndex");
|
bind(lib.disposeIndex, "clang_disposeIndex");
|
||||||
bind(lib.ParseTranslationUnit, "clang_parseTranslationUnit");
|
bind(lib.parseTranslationUnit, "clang_parseTranslationUnit");
|
||||||
bind(lib.DisposeTranslationUnit, "clang_disposeTranslationUnit");
|
bind(lib.disposeTranslationUnit, "clang_disposeTranslationUnit");
|
||||||
bind(lib.GetFile, "clang_getFile");
|
bind(lib.getFile, "clang_getFile");
|
||||||
bind(lib.GetLocationForOffset, "clang_getLocationForOffset");
|
bind(lib.getLocationForOffset, "clang_getLocationForOffset");
|
||||||
bind(lib.GetRange, "clang_getRange");
|
bind(lib.getRange, "clang_getRange");
|
||||||
bind(lib.GetRangeStart, "clang_getRangeStart");
|
bind(lib.getRangeStart, "clang_getRangeStart");
|
||||||
bind(lib.GetRangeEnd, "clang_getRangeEnd");
|
bind(lib.getRangeEnd, "clang_getRangeEnd");
|
||||||
bind(lib.GetFileLocation, "clang_getFileLocation");
|
bind(lib.getFileLocation, "clang_getFileLocation");
|
||||||
bind(lib.Tokenize, "clang_tokenize");
|
bind(lib.tokenize, "clang_tokenize");
|
||||||
bind(lib.DisposeTokens, "clang_disposeTokens");
|
bind(lib.disposeTokens, "clang_disposeTokens");
|
||||||
bind(lib.GetTokenKind, "clang_getTokenKind");
|
bind(lib.getTokenKind, "clang_getTokenKind");
|
||||||
bind(lib.GetTokenExtent, "clang_getTokenExtent");
|
bind(lib.getTokenExtent, "clang_getTokenExtent");
|
||||||
bind(lib.GetTranslationUnitCursor, "clang_getTranslationUnitCursor");
|
bind(lib.getTranslationUnitCursor, "clang_getTranslationUnitCursor");
|
||||||
bind(lib.VisitChildren, "clang_visitChildren");
|
bind(lib.visitChildren, "clang_visitChildren");
|
||||||
bind(lib.GetCursorKind, "clang_getCursorKind");
|
bind(lib.getCursorKind, "clang_getCursorKind");
|
||||||
bind(lib.GetCursorSpelling, "clang_getCursorSpelling");
|
bind(lib.getCursorSpelling, "clang_getCursorSpelling");
|
||||||
bind(lib.GetCursorType, "clang_getCursorType");
|
bind(lib.getCursorType, "clang_getCursorType");
|
||||||
bind(lib.GetTypeSpelling, "clang_getTypeSpelling");
|
bind(lib.getTypeSpelling, "clang_getTypeSpelling");
|
||||||
bind(lib.GetCursorLocation, "clang_getCursorLocation");
|
bind(lib.getCursorLocation, "clang_getCursorLocation");
|
||||||
bind(lib.GetCursorExtent, "clang_getCursorExtent");
|
bind(lib.getCursorExtent, "clang_getCursorExtent");
|
||||||
bind(lib.GetCursorReferenced, "clang_getCursorReferenced");
|
bind(lib.getCursorReferenced, "clang_getCursorReferenced");
|
||||||
bind(lib.GetStorageClass, "clang_Cursor_getStorageClass");
|
bind(lib.getStorageClass, "clang_Cursor_getStorageClass");
|
||||||
bind(lib.EnumDeclIsScoped, "clang_EnumDecl_isScoped");
|
bind(lib.enumDeclIsScoped, "clang_EnumDecl_isScoped");
|
||||||
bind(lib.IsCursorDefinition, "clang_isCursorDefinition");
|
bind(lib.isCursorDefinition, "clang_isCursorDefinition");
|
||||||
bind(lib.LocationIsFromMainFile, "clang_Location_isFromMainFile");
|
bind(lib.locationIsFromMainFile, "clang_Location_isFromMainFile");
|
||||||
bind(lib.GetNumDiagnostics, "clang_getNumDiagnostics");
|
bind(lib.getNumDiagnostics, "clang_getNumDiagnostics");
|
||||||
bind(lib.GetDiagnostic, "clang_getDiagnostic");
|
bind(lib.getDiagnostic, "clang_getDiagnostic");
|
||||||
bind(lib.GetDiagnosticSeverity, "clang_getDiagnosticSeverity");
|
bind(lib.getDiagnosticSeverity, "clang_getDiagnosticSeverity");
|
||||||
bind(lib.GetDiagnosticSpelling, "clang_getDiagnosticSpelling");
|
bind(lib.getDiagnosticSpelling, "clang_getDiagnosticSpelling");
|
||||||
bind(lib.DisposeDiagnostic, "clang_disposeDiagnostic");
|
bind(lib.disposeDiagnostic, "clang_disposeDiagnostic");
|
||||||
bind(lib.GetCString, "clang_getCString");
|
bind(lib.getCString, "clang_getCString");
|
||||||
bind(lib.DisposeString, "clang_disposeString");
|
bind(lib.disposeString, "clang_disposeString");
|
||||||
bind(lib.GetFileName, "clang_getFileName");
|
bind(lib.getFileName, "clang_getFileName");
|
||||||
bind(lib.CursorIsNull, "clang_Cursor_isNull");
|
bind(lib.cursorIsNull, "clang_Cursor_isNull");
|
||||||
bind(lib.GetResultType, "clang_getResultType");
|
bind(lib.getResultType, "clang_getResultType");
|
||||||
if (!missing.empty()) {
|
if (!missing.empty()) {
|
||||||
lib.handle = nullptr;
|
lib.handle = nullptr;
|
||||||
lib.error = std::format("loaded {} but it is missing {}", candidates.front(), join(missing));
|
lib.error = std::format("loaded {} but it is missing {}", candidates.front(), join(missing));
|
||||||
|
|
@ -229,36 +229,36 @@ namespace {
|
||||||
unsaved.Contents = content.data();
|
unsaved.Contents = content.data();
|
||||||
unsaved.Length = static_cast<std::uint32_t>(content.size());
|
unsaved.Length = static_cast<std::uint32_t>(content.size());
|
||||||
|
|
||||||
CXIndex index = lc.CreateIndex(0, 0);
|
CXIndex index = lc.createIndex(0, 0);
|
||||||
if (!index) return {};
|
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) {
|
if (!tu) {
|
||||||
lc.DisposeIndex(index);
|
lc.disposeIndex(index);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<LintToken> tokens;
|
std::vector<LintToken> tokens;
|
||||||
if (CXFile cxFile = lc.GetFile(tu, path.c_str())) {
|
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())));
|
CXSourceRange whole = lc.getRange(lc.getLocationForOffset(tu, cxFile, 0), lc.getLocationForOffset(tu, cxFile, static_cast<std::uint32_t>(content.size())));
|
||||||
CXToken* raw = nullptr;
|
CXToken* raw = nullptr;
|
||||||
std::uint32_t count = 0;
|
std::uint32_t count = 0;
|
||||||
lc.Tokenize(tu, whole, &raw, &count);
|
lc.tokenize(tu, whole, &raw, &count);
|
||||||
tokens.reserve(count);
|
tokens.reserve(count);
|
||||||
for (std::uint32_t i = 0; i < count; ++i) {
|
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 line = 0;
|
||||||
std::uint32_t column = 0;
|
std::uint32_t column = 0;
|
||||||
std::uint32_t begin = 0;
|
std::uint32_t begin = 0;
|
||||||
std::uint32_t end = 0;
|
std::uint32_t end = 0;
|
||||||
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, &line, &column, &begin);
|
lc.getFileLocation(lc.getRangeStart(extent), nullptr, &line, &column, &begin);
|
||||||
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &end);
|
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &end);
|
||||||
if (end < begin || begin > content.size()) continue;
|
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.disposeTranslationUnit(tu);
|
||||||
lc.DisposeIndex(index);
|
lc.disposeIndex(index);
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,9 +269,9 @@ namespace {
|
||||||
// works this out for itself now: the initialiser resolves to a
|
// works this out for itself now: the initialiser resolves to a
|
||||||
// declaration in clang-c/, outside the project, so the declaration is
|
// declaration in clang-c/, outside the project, so the declaration is
|
||||||
// flagged as foreign API and exempt. No suppression comment needed.
|
// 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 : "";
|
std::string out = raw ? raw : "";
|
||||||
lc.DisposeString(s);
|
lc.disposeString(s);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,17 +386,17 @@ namespace {
|
||||||
// from where the declaration actually lives.
|
// from where the declaration actually lives.
|
||||||
bool ResolvesOutsideProject(const LibClang& lc, CXCursor cursor, const fs::path& projectRoot) {
|
bool ResolvesOutsideProject(const LibClang& lc, CXCursor cursor, const fs::path& projectRoot) {
|
||||||
if (projectRoot.empty()) return false;
|
if (projectRoot.empty()) return false;
|
||||||
CXCursor target = lc.GetCursorReferenced(cursor);
|
CXCursor target = lc.getCursorReferenced(cursor);
|
||||||
if (lc.CursorIsNull(target)) return false;
|
if (lc.cursorIsNull(target)) return false;
|
||||||
CXSourceLocation location = lc.GetCursorLocation(target);
|
CXSourceLocation location = lc.getCursorLocation(target);
|
||||||
if (lc.LocationIsFromMainFile(location)) return false;
|
if (lc.locationIsFromMainFile(location)) return false;
|
||||||
CXFile file = nullptr;
|
CXFile file = nullptr;
|
||||||
std::uint32_t line = 0;
|
std::uint32_t line = 0;
|
||||||
std::uint32_t column = 0;
|
std::uint32_t column = 0;
|
||||||
std::uint32_t offset = 0;
|
std::uint32_t offset = 0;
|
||||||
lc.GetFileLocation(location, &file, &line, &column, &offset);
|
lc.getFileLocation(location, &file, &line, &column, &offset);
|
||||||
if (!file) return false;
|
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;
|
if (path.empty()) return false;
|
||||||
return !PathInsideRoot(fs::path(path), projectRoot);
|
return !PathInsideRoot(fs::path(path), projectRoot);
|
||||||
}
|
}
|
||||||
|
|
@ -404,13 +404,13 @@ namespace {
|
||||||
CXChildVisitResult VisitDecl(CXCursor cursor, CXCursor, CXClientData data) {
|
CXChildVisitResult VisitDecl(CXCursor cursor, CXCursor, CXClientData data) {
|
||||||
DeclWalk& walk = *static_cast<DeclWalk*>(data);
|
DeclWalk& walk = *static_cast<DeclWalk*>(data);
|
||||||
const LibClang& lc = *walk.lc;
|
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 —
|
// Every declaration the imported modules bring in arrives here too —
|
||||||
// an unfiltered visit of one interface unit walks ~495,000 cursors from
|
// an unfiltered visit of one interface unit walks ~495,000 cursors from
|
||||||
// std alone. Prune before doing any work.
|
// 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);
|
LintDeclKind mapped = MapCursorKind(kind);
|
||||||
if (mapped == LintDeclKind::Other) {
|
if (mapped == LintDeclKind::Other) {
|
||||||
// A foreign reference inside a VARIABLE, FIELD or PARAMETER is an
|
// 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
|
// enclosing-declaration stack stays accurate: the callback is never
|
||||||
// told when a subtree ends.
|
// told when a subtree ends.
|
||||||
if (kind == CXCursor_LinkageSpec) {
|
if (kind == CXCursor_LinkageSpec) {
|
||||||
CXSourceRange extent = lc.GetCursorExtent(cursor);
|
CXSourceRange extent = lc.getCursorExtent(cursor);
|
||||||
std::uint32_t specBegin = 0;
|
std::uint32_t specBegin = 0;
|
||||||
std::uint32_t specEnd = 0;
|
std::uint32_t specEnd = 0;
|
||||||
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, nullptr, nullptr, &specBegin);
|
lc.getFileLocation(lc.getRangeStart(extent), nullptr, nullptr, nullptr, &specBegin);
|
||||||
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &specEnd);
|
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &specEnd);
|
||||||
std::string_view text;
|
std::string_view text;
|
||||||
if (specBegin < walk.content->size()) {
|
if (specBegin < walk.content->size()) {
|
||||||
text = std::string_view(walk.content->data() + specBegin, std::min<std::size_t>(specEnd - specBegin, walk.content->size() - specBegin));
|
text = std::string_view(walk.content->data() + specBegin, std::min<std::size_t>(specEnd - specBegin, walk.content->size() - specBegin));
|
||||||
}
|
}
|
||||||
bool isC = IsExternCLinkage(text);
|
bool isC = IsExternCLinkage(text);
|
||||||
if (isC) ++walk.externCDepth;
|
if (isC) ++walk.externCDepth;
|
||||||
lc.VisitChildren(cursor, &VisitDecl, &walk);
|
lc.visitChildren(cursor, &VisitDecl, &walk);
|
||||||
if (isC) --walk.externCDepth;
|
if (isC) --walk.externCDepth;
|
||||||
return CXChildVisit_Continue;
|
return CXChildVisit_Continue;
|
||||||
}
|
}
|
||||||
lc.VisitChildren(cursor, &VisitDecl, &walk);
|
lc.visitChildren(cursor, &VisitDecl, &walk);
|
||||||
return CXChildVisit_Continue;
|
return CXChildVisit_Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LintDecl decl;
|
LintDecl decl;
|
||||||
decl.kind = mapped;
|
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
|
// For anything callable, `type` is the RESULT type rather than the
|
||||||
// whole function type: the parameters arrive as their own Parameter
|
// whole function type: the parameters arrive as their own Parameter
|
||||||
// declarations, so spelling them here too would make every rule
|
// declarations, so spelling them here too would make every rule
|
||||||
// reading `type` report each one twice.
|
// reading `type` report each one twice.
|
||||||
bool callable = mapped == LintDeclKind::Function || mapped == LintDeclKind::Method;
|
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;
|
CXFile nameFile = nullptr;
|
||||||
std::uint32_t nameLine = 0;
|
std::uint32_t nameLine = 0;
|
||||||
std::uint32_t nameColumn = 0;
|
std::uint32_t nameColumn = 0;
|
||||||
std::uint32_t nameOffset = 0;
|
std::uint32_t nameOffset = 0;
|
||||||
lc.GetFileLocation(location, &nameFile, &nameLine, &nameColumn, &nameOffset);
|
lc.getFileLocation(location, &nameFile, &nameLine, &nameColumn, &nameOffset);
|
||||||
decl.line = nameLine;
|
decl.line = nameLine;
|
||||||
decl.column = nameColumn;
|
decl.column = nameColumn;
|
||||||
CXSourceRange extent = lc.GetCursorExtent(cursor);
|
CXSourceRange extent = lc.getCursorExtent(cursor);
|
||||||
std::uint32_t begin = 0;
|
std::uint32_t begin = 0;
|
||||||
std::uint32_t end = 0;
|
std::uint32_t end = 0;
|
||||||
lc.GetFileLocation(lc.GetRangeStart(extent), nullptr, nullptr, nullptr, &begin);
|
lc.getFileLocation(lc.getRangeStart(extent), nullptr, nullptr, nullptr, &begin);
|
||||||
lc.GetFileLocation(lc.GetRangeEnd(extent), nullptr, nullptr, nullptr, &end);
|
lc.getFileLocation(lc.getRangeEnd(extent), nullptr, nullptr, nullptr, &end);
|
||||||
decl.begin = begin;
|
decl.begin = begin;
|
||||||
decl.end = std::min<std::size_t>(end, walk.content->size());
|
decl.end = std::min<std::size_t>(end, walk.content->size());
|
||||||
decl.isDefinition = lc.IsCursorDefinition(cursor) != 0;
|
decl.isDefinition = lc.isCursorDefinition(cursor) != 0;
|
||||||
decl.isStatic = lc.GetStorageClass(cursor) == CX_SC_Static;
|
decl.isStatic = lc.getStorageClass(cursor) == CX_SC_Static;
|
||||||
decl.isScopedEnum = mapped == LintDeclKind::Enum && lc.EnumDeclIsScoped(cursor) != 0;
|
decl.isScopedEnum = mapped == LintDeclKind::Enum && lc.enumDeclIsScoped(cursor) != 0;
|
||||||
// Inside an extern "C" block, where a C API's spelling is not ours to
|
// Inside an extern "C" block, where a C API's spelling is not ours to
|
||||||
// modernise. NOT clang_getCursorLanguage: its default answer for a
|
// modernise. NOT clang_getCursorLanguage: its default answer for a
|
||||||
// plain function, variable or parameter is CXLanguage_C even in a C++
|
// 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.out->push_back(std::move(decl));
|
||||||
walk.stack.push_back(walk.out->size() - 1);
|
walk.stack.push_back(walk.out->size() - 1);
|
||||||
lc.VisitChildren(cursor, &VisitDecl, &walk);
|
lc.visitChildren(cursor, &VisitDecl, &walk);
|
||||||
walk.stack.pop_back();
|
walk.stack.pop_back();
|
||||||
return CXChildVisit_Continue;
|
return CXChildVisit_Continue;
|
||||||
}
|
}
|
||||||
|
|
@ -504,7 +504,7 @@ namespace {
|
||||||
walk.content = &content;
|
walk.content = &content;
|
||||||
walk.projectRoot = &projectRoot;
|
walk.projectRoot = &projectRoot;
|
||||||
walk.out = &decls;
|
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
|
// A declaration on an interop boundary makes its parameters and fields
|
||||||
// interop too — the exemption has to cover the whole signature, not
|
// interop too — the exemption has to cover the whole signature, not
|
||||||
// just the node that happened to name the foreign entity.
|
// just the node that happened to name the foreign entity.
|
||||||
|
|
@ -579,34 +579,34 @@ namespace {
|
||||||
unsaved.Contents = buffer.data();
|
unsaved.Contents = buffer.data();
|
||||||
unsaved.Length = static_cast<std::uint32_t>(buffer.size());
|
unsaved.Length = static_cast<std::uint32_t>(buffer.size());
|
||||||
|
|
||||||
CXIndex index = lc.CreateIndex(0, 0);
|
CXIndex index = lc.createIndex(0, 0);
|
||||||
if (!index) {
|
if (!index) {
|
||||||
result.error = "clang_createIndex failed";
|
result.error = "clang_createIndex failed";
|
||||||
return result;
|
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) {
|
if (!tu) {
|
||||||
lc.DisposeIndex(index);
|
lc.disposeIndex(index);
|
||||||
result.error = "clang could not create a translation unit";
|
result.error = "clang could not create a translation unit";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string fatal;
|
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) {
|
for (std::uint32_t i = 0; i < diagnostics && fatal.empty(); ++i) {
|
||||||
CXDiagnostic diagnostic = lc.GetDiagnostic(tu, i);
|
CXDiagnostic diagnostic = lc.getDiagnostic(tu, i);
|
||||||
if (lc.GetDiagnosticSeverity(diagnostic) == CXDiagnostic_Fatal) {
|
if (lc.getDiagnosticSeverity(diagnostic) == CXDiagnostic_Fatal) {
|
||||||
fatal = TakeString(lc, lc.GetDiagnosticSpelling(diagnostic));
|
fatal = TakeString(lc, lc.getDiagnosticSpelling(diagnostic));
|
||||||
}
|
}
|
||||||
lc.DisposeDiagnostic(diagnostic);
|
lc.disposeDiagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
if (fatal.empty()) {
|
if (fatal.empty()) {
|
||||||
result.decls = WalkDecls(lc, tu, buffer, projectRoot);
|
result.decls = WalkDecls(lc, tu, buffer, projectRoot);
|
||||||
} else {
|
} else {
|
||||||
result.error = std::move(fatal);
|
result.error = std::move(fatal);
|
||||||
}
|
}
|
||||||
lc.DisposeTranslationUnit(tu);
|
lc.disposeTranslationUnit(tu);
|
||||||
lc.DisposeIndex(index);
|
lc.disposeIndex(index);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,12 +71,12 @@ namespace Crafter {
|
||||||
std::string src = contents.str();
|
std::string src = contents.str();
|
||||||
|
|
||||||
std::string pathStr = path.string();
|
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 char* shaderSource = src.data();
|
||||||
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
||||||
|
|
||||||
glslang::TShader shader(glslangType);
|
glslang::TShader shader(glslangType);
|
||||||
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, file_name_list, 1);
|
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1);
|
||||||
shader.setEntryPoint(entrypoint.c_str());
|
shader.setEntryPoint(entrypoint.c_str());
|
||||||
shader.setSourceEntryPoint(entrypoint.c_str());
|
shader.setSourceEntryPoint(entrypoint.c_str());
|
||||||
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);
|
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);
|
||||||
|
|
|
||||||
190
lint-rules.h
190
lint-rules.h
|
|
@ -82,15 +82,6 @@ inline bool IsCharPointer(std::string_view type) {
|
||||||
return base == "char";
|
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) {
|
inline void AddProjectLintRules(Crafter::Configuration& cfg) {
|
||||||
using Crafter::LintContext;
|
using Crafter::LintContext;
|
||||||
|
|
||||||
|
|
@ -118,132 +109,77 @@ inline void AddProjectLintRules(Crafter::Configuration& cfg) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Naming: functions/types PascalCase, variables camelCase, statics and
|
// Naming: types and functions PascalCase, variables camelCase, with
|
||||||
// namespace-scope globals PascalCase. A line-based scope tracker decides
|
// statics, namespace-scope globals and constexpr constants PascalCase.
|
||||||
// whether a declaration sits at namespace scope (global) or inside a
|
//
|
||||||
// function/type (local/member). Heuristic by nature — report-only.
|
// Reads the AST. The previous version needed four std::regex, a hand-rolled
|
||||||
cfg.AddLintRule("naming", [](LintContext& ctx) {
|
// {/} 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;
|
if (!IsCppFile(ctx)) return;
|
||||||
std::vector<std::string_view> lines = Lines(ctx.CommentStripped());
|
using Kind = Crafter::LintDeclKind;
|
||||||
|
std::span<const Crafter::LintDecl> decls = ctx.Decls();
|
||||||
enum class Scope { Namespace, Type, Function, Other };
|
for (const Crafter::LintDecl& decl : decls) {
|
||||||
std::vector<Scope> stack;
|
if (decl.name.empty()) continue; // anonymous namespace, unnamed struct
|
||||||
auto currentScope = [&]() { return stack.empty() ? Scope::Namespace : stack.back(); };
|
// A declaration with C language linkage is named by the C library
|
||||||
|
// it mirrors — `extern "C" int setenv(...)` is not ours to rename.
|
||||||
static const std::regex typeDecl(R"(\b(?:class|struct|union)\s+(?:CRAFTER_API\s+)?([A-Za-z_]\w*))");
|
if (decl.isExternC) continue;
|
||||||
static const std::regex enumDecl(R"(\benum\s+(?:class\s+|struct\s+)?([A-Za-z_]\w*))");
|
// Namespace scope is now a lookup rather than a brace count, and it
|
||||||
static const std::regex usingDecl(R"(^\s*using\s+([A-Za-z_]\w*)\s*=)");
|
// is exact for a local declared inside a function body.
|
||||||
static const std::regex varDecl(
|
bool atNamespaceScope = decl.parent == Crafter::LintNoParent
|
||||||
R"(^\s*((?:static|constexpr|const|inline|mutable|thread_local|export|CRAFTER_API)\s+)*)"
|
|| decls[decl.parent].kind == Kind::Namespace;
|
||||||
R"((?:std::)?[A-Za-z_][\w:]*(?:<[^;={]*>)?(?:\s*[&*])*\s+([A-Za-z_]\w*)\s*(=|;|\{))");
|
switch (decl.kind) {
|
||||||
static const std::unordered_set<std::string_view> keywords = {
|
case Kind::Class:
|
||||||
"if", "for", "while", "switch", "catch", "return", "else", "do", "case", "goto",
|
case Kind::Struct:
|
||||||
"new", "delete", "throw", "using", "namespace", "template", "typedef", "friend",
|
case Kind::Union:
|
||||||
"public", "private", "protected", "class", "struct", "enum", "union", "import",
|
case Kind::Enum:
|
||||||
"module", "export", "break", "continue", "co_return", "co_await", "co_yield",
|
if (!IsPascalCase(decl.name)) {
|
||||||
"sizeof", "alignof", "decltype", "static_assert", "operator", "try", "requires",
|
ctx.Report(decl.line, std::format("type '{}' should be PascalCase", decl.name));
|
||||||
};
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case Kind::TypeAlias:
|
||||||
|
if (!IsPascalCase(decl.name)) {
|
||||||
|
ctx.Report(decl.line, std::format("type alias '{}' should be PascalCase", decl.name));
|
||||||
}
|
}
|
||||||
if (std::regex_search(lineStr, m, usingDecl) && !IsPascalCase(m[1].str())) {
|
break;
|
||||||
ctx.Report(i + 1, std::format("type alias '{}' should be PascalCase", m[1].str()));
|
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;
|
||||||
// Function definitions: identifier before the first '(' on a
|
case Kind::Variable:
|
||||||
// line that ends where a body opens or closes — a multi-line
|
// constexpr variables are compile-time constants and take
|
||||||
// def's trailing '{' or a one-liner's trailing '}'. Statement
|
// constant naming, like statics and globals.
|
||||||
// calls with inline lambda arguments look similar
|
if (decl.isStatic || decl.isConstexpr || atNamespaceScope) {
|
||||||
// (`bool x = std::any_of(..., [](T v) { ... });`) but end in
|
if (!IsPascalCase(decl.name)) {
|
||||||
// ';' and/or carry '=' before the name — both excluded.
|
ctx.Report(decl.line, std::format("{} '{}' should be PascalCase",
|
||||||
// Lambdas ("](") and control keywords are skipped; ctors/
|
decl.isStatic ? "static variable"
|
||||||
// dtors pass the Pascal check by construction; `main` and
|
: decl.isConstexpr ? "constexpr constant"
|
||||||
// operators exempt.
|
: "global variable", decl.name));
|
||||||
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("::"));
|
|
||||||
}
|
}
|
||||||
if (looksLikeDef && !IsPascalCase(name)) {
|
} else if (!IsCamelCase(decl.name)) {
|
||||||
ctx.Report(i + 1, std::format("function '{}' should be PascalCase", 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));
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
// Variable declarations: camelCase locally, PascalCase for
|
default:
|
||||||
// statics and namespace-scope globals.
|
break;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,42 @@ int main() {
|
||||||
Check(HasFinding(r.summary, "'raw'"), "no-char-pointer: a deduced char* local is still ours");
|
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) {
|
if (Failures > 0) {
|
||||||
std::println(std::cerr, "{} assertions failed", Failures);
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue