Merge pull request #3219 from DHowett/custom-clang-in-ci

ci: add a new Lint step that uses a custom-built linter
This commit is contained in:
Dustin L. Howett authored and GitHub committed 2026-07-04 14:57:14 -05:00
commit afc33dfeb4
5 files changed
+303

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
name: Lint
on: [push, pull_request]
permissions:
contents: read
jobs:
Lint:
runs-on: ubuntu-26.04
env:
BS: cmake
CRYPTO: openssl
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Update apt cache
run: sudo apt-get update
- name: Install dependencies
run: sudo apt-get install -y autoconf automake build-essential cmake git libssl-dev nettle-dev libmbedtls-dev libacl1-dev libbz2-dev liblzma-dev liblz4-dev libzstd-dev lzop pkg-config zlib1g-dev libclang-22-dev libclang-cpp22-dev clang-22 ninja-build
- name: Install Lint
run: |-
set -euo pipefail
cmake -S build/la-lint -B /tmp/lint-build -GNinja
cmake --build /tmp/lint-build
cmake --install /tmp/lint-build
- name: Configure
run: ./build/ci/build.sh -a configure
env:
CMAKE_ARGS: -GNinja -DENABLE_CLANG_TIDY=YES -DCLANG_TIDY_PATH=la-lint
- name: Build
run: ./build/ci/build.sh -a build
env:
MAKE: ninja
MAKE_ARGS: --
+8
View File
@@ -254,6 +254,7 @@ OPTION(ENABLE_ICONV "Enable iconv support" ON)
OPTION(ENABLE_TEST "Enable unit and regression tests" ON)
OPTION(ENABLE_COVERAGE "Enable code coverage (GCC only, automatically sets ENABLE_TEST to ON)" FALSE)
OPTION(ENABLE_INSTALL "Enable installing of libraries" ON)
OPTION(ENABLE_CLANG_TIDY "Enable custom build-time code health checks" OFF)
IF(WIN32 AND MSVC)
OPTION(MSVC_USE_STATIC_CRT "Use static CRT" OFF)
@@ -2229,6 +2230,13 @@ IF(ENABLE_TEST)
ADD_CUSTOM_TARGET(run_all_tests)
ENDIF(ENABLE_TEST)
IF(ENABLE_CLANG_TIDY)
IF(NOT CLANG_TIDY_PATH)
SET(CLANG_TIDY_PATH clang-tidy)
ENDIF(NOT CLANG_TIDY_PATH)
SET(CMAKE_C_CLANG_TIDY ${CLANG_TIDY_PATH})
ENDIF(ENABLE_CLANG_TIDY)
add_subdirectory(libarchive)
add_subdirectory(cat)
add_subdirectory(tar)
+46
View File
@@ -0,0 +1,46 @@
---
BasedOnStyle: Google
AlignAfterOpenBracket: DontAlign
AlignArrayOfStructures: Left
AlignConsecutiveAssignments:
AlignFunctionDeclarations: true
AlignConsecutiveMacros:
Enabled: true
AlignOperands: DontAlign
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Empty
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: false
BinPackParameters: OnePerLine
BreakBeforeTemplateCloser: true
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterComma
BreakStringLiterals: false
ColumnLimit: 120
ConstructorInitializerIndentWidth: 8
EnumTrailingComma: Insert
IncludeBlocks: Preserve
IndentCaseLabels: false
IndentGotoLabels: false
IndentPPDirectives: AfterHash
IndentWidth: 8
PPIndentWidth: 1
PackConstructorInitializers: BinPack
PointerAlignment: Right
QualifierAlignment: Left
RemoveEmptyLinesInUnwrappedLines: true
SeparateDefinitionBlocks: Always
SortIncludes:
Enabled: false
SpaceBeforeParens: Custom
SpaceBeforeParensOptions:
AfterControlStatements: false
AfterForeachMacros: false
AfterIfMacros: false
SpacesInContainerLiterals: false
UseTab: ForIndentation
+25
View File
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.12)
set(CMAKE_CXX_STANDARD 23)
project(libarchive-clang-tools)
find_package(zstd REQUIRED)
find_package(Clang REQUIRED)
add_executable(la-lint)
target_sources(la-lint
PRIVATE
main.cpp
)
target_include_directories(la-lint
PRIVATE
${CLANG_INCLUDE_DIRS}
)
target_link_libraries(la-lint
PRIVATE
LLVM LLVMCore LLVMSupport
libclang clang-cpp
)
install(TARGETS la-lint RUNTIME DESTINATION bin)
+190
View File
@@ -0,0 +1,190 @@
/*-
* Copyright (c) 2026 Dustin L. Howett
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer
* in this position and unchanged.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <clang/Frontend/FrontendActions.h>
#include <clang/Frontend/TextDiagnosticPrinter.h>
#include <clang/Frontend/ChainedDiagnosticConsumer.h>
#include <clang/Tooling/CommonOptionsParser.h>
#include <clang/Tooling/Tooling.h>
#include <clang/ASTMatchers/ASTMatchFinder.h>
#include <llvm/Support/CommandLine.h>
#include <print>
using namespace clang::tooling;
using namespace clang::ast_matchers;
using namespace llvm;
static llvm::cl::OptionCategory LaToolCategory("la-clang-tool options");
static llvm::cl::opt<std::string> Prefix("prefix",
llvm::cl::desc("Project path prefix"),
llvm::cl::init(""),
llvm::cl::cat(LaToolCategory));
static bool IsGithubCiBuild{};
struct MemCmpNeedleHandler : public MatchFinder::MatchCallback {
virtual void run(const MatchFinder::MatchResult &result) {
auto needle = result.Nodes.getNodeAs<clang::StringLiteral>("needle");
auto lenExpr = result.Nodes.getNodeAs<clang::IntegerLiteral>("length");
/*
* This check uses [less than] rather than [less equal] because in some formats we want to verify the
* presence of the null terminator and in others we do not.
*/
if(lenExpr->getValue().ult(needle->getLength())) {
auto &diag = result.Context->getDiagnostics();
auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Error,
"memcmp needle has length `%0', but is checked with length `%1'");
clang::DiagnosticBuilder db = diag.Report(lenExpr->getLocation(), did);
db.AddTaggedVal(needle->getLength(), clang::DiagnosticsEngine::ArgumentKind::ak_uint);
db.AddTaggedVal(lenExpr->getValue().getSExtValue(),
clang::DiagnosticsEngine::ArgumentKind::ak_sint);
db.AddSourceRange(clang::CharSourceRange::getCharRange(needle->getSourceRange()));
}
}
};
struct AssertUAFHandler : public MatchFinder::MatchCallback {
virtual void run(const MatchFinder::MatchResult &result) {
/*
* This code uses getFileLoc to specify locations disregarding macro expansion
* and to avoid Clang printing "macro expanded from here".
*/
auto &SM = *result.SourceManager;
auto uaf = result.Nodes.getNodeAs<clang::DeclRefExpr>("uaf");
auto assertion = result.Nodes.getNodeAs<clang::CallExpr>("assertion");
auto freed = result.Nodes.getNodeAs<clang::CallExpr>("free");
auto &diag = result.Context->getDiagnostics();
{
auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Error,
"test assertion uses `%0' after free; prefer assertEqualInt");
clang::DiagnosticBuilder db = diag.Report(SM.getFileLoc(uaf->getBeginLoc()), did);
db.AddString(uaf->getNameInfo().getAsString());
db << clang::FixItHint::CreateReplacement(
clang::SourceRange{SM.getFileLoc(assertion->getBeginLoc()),
SM.getFileLoc(uaf->getBeginLoc())},
"assertEqualInt(");
}
{
auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Note, "`%0' freed here");
clang::DiagnosticBuilder db = diag.Report(freed->getBeginLoc(), did);
db.AddString(uaf->getNameInfo().getAsString());
}
}
};
class GithubActionDiagnosticConsumer : public clang::DiagnosticConsumer {
virtual void HandleDiagnostic(clang::DiagnosticsEngine::Level DiagLevel, const clang::Diagnostic &Info) {
static constexpr const char *levels[] = {nullptr, "notice", "notice", "warning", "error", "error"};
const auto level = levels[DiagLevel];
if(!level)
return;
auto &SM = Info.getSourceManager();
auto loc = Info.getLocation();
loc = SM.getFileLoc(loc);
auto file = SM.getFilename(loc);
if(file.empty()) {
return;
}
file.consume_front_insensitive(Prefix);
while(file.consume_front("/")) {}
bool lineInvalid = false;
auto line = SM.getLineNumber(loc, &lineInvalid);
if(lineInvalid) {
return;
}
llvm::SmallVector<char> diagString;
Info.FormatDiagnostic(diagString);
std::print("::{0} file={1},line={2},title=Lint::{3}\n", level, std::string_view{file}, line,
std::string_view{diagString});
}
};
int main(int argc, const char **argv) {
auto exp = CommonOptionsParser::create(argc, argv, LaToolCategory);
auto &optionsParser = exp.get();
ClangTool tool(optionsParser.getCompilations(), optionsParser.getSourcePathList());
if(const char *e = getenv("GITHUB_WORKSPACE")) {
IsGithubCiBuild = true;
if(Prefix.c_str()[0] == '\0') {
Prefix.assign(e);
Prefix.append("/");
}
}
MatchFinder finder;
/*
* Find every assertion_equal_mem that passes a string literal.
* --> assertEqualMem(foo, "literal", 7);
*/
MemCmpNeedleHandler memCmpHandler;
finder.addMatcher(callExpr(callee(functionDecl(hasName("assertion_equal_mem"))),
hasArgument(4, stringLiteral().bind("needle")),
hasArgument(6, integerLiteral(anything()).bind("length"))),
&memCmpHandler);
/*
* Find every assertion_equal_int whose call site contains a `free` function,
* and then passes the freed decl to assertion_equal_int as context.
* --> assertEqualIntA(a, ARCHIVE_OK, archive_write_free(a));
* ^-- must be the same as ----------^
*/
AssertUAFHandler uafHandler;
finder.addMatcher(
callExpr(callee(functionDecl(hasName("assertion_equal_int"))),
hasArgument(4,
callExpr(callee(functionDecl(matchesName("archive.*_free$"))),
hasArgument(0, declRefExpr(to(varDecl(anything()).bind("freed")))))
.bind("free")),
hasArgument(6, declRefExpr(to(varDecl(declaresSameEntityAsBoundNode("freed")))).bind("uaf")))
.bind("assertion"),
&uafHandler);
std::unique_ptr<clang::DiagnosticConsumer> diagnosticConsumer;
if(IsGithubCiBuild) {
/* Emit GitHub-formatted error messages by default when we are building in GitHub Actions */
clang::DiagnosticOptions diagOptions;
diagOptions.ShowColors = true;
clang::TextDiagnosticPrinter defaultDiag(llvm::errs(), diagOptions, false);
diagnosticConsumer = std::make_unique<clang::ChainedDiagnosticConsumer>(&defaultDiag,
std::make_unique<GithubActionDiagnosticConsumer>());
tool.setDiagnosticConsumer(diagnosticConsumer.get());
}
return tool.run(newFrontendActionFactory(&finder).get());
}