https://github.com/jkorous-apple updated https://github.com/llvm/llvm-project/pull/203660
>From 577a6f69afd9d3aa4745a9a0974ef1b010bc8df5 Mon Sep 17 00:00:00 2001 From: Jan Korous <[email protected]> Date: Fri, 12 Jun 2026 19:15:00 -0700 Subject: [PATCH 1/4] [clang][ssaf] Add source-transformation library scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the abstract base classes, registries, and force-linker anchor for the SSAF source-transformation library. A `Transformation` is an `ASTConsumer` that consumes a previously computed `WPASuite` and emits source edits and findings through two sinks: `SourceEditEmitter` (accumulates `clang::tooling::Replacement`s) and `TransformationReportEmitter` (accumulates `(ruleId, level, range, message)` tuples). The accumulated state is then handed to a `SourceEditFormat` and a `TransformationReportFormat` for serialization. Three `llvm::Registry`-backed registries — keyed by transformation name and by file extension respectively — let transformations and formats be linked in statically (with a force-linker anchor) or loaded dynamically as a clang plugin. Each registry exposes the standard `is*Registered` / `make*` / `printAvailable*` helpers used elsewhere in SSAF. No transformation or format ships yet; this commit only adds the plumbing. Existing tools (`clang-ssaf-format`, `clang-ssaf-linker`, `clang-ssaf-analyzer`) and the clang driver gain the new lib in their link line so the framework anchor resolves under static linking. Assisted-By: Claude Opus 4.7 --- .../BuiltinAnchorSources.def | 1 + .../SourceTransformation/SourceEditEmitter.h | 29 +++++++ .../SourceTransformation/Transformation.h | 38 +++++++++ .../TransformationRegistry.h | 66 +++++++++++++++ .../TransformationReportEmitter.h | 35 ++++++++ clang/lib/Driver/CMakeLists.txt | 1 + clang/lib/FrontendTool/CMakeLists.txt | 1 + .../CMakeLists.txt | 1 + .../SourceTransformation/CMakeLists.txt | 13 +++ .../TransformationRegistry.cpp | 44 ++++++++++ .../tools/clang-ssaf-analyzer/CMakeLists.txt | 1 + clang/tools/clang-ssaf-format/CMakeLists.txt | 1 + clang/tools/clang-ssaf-linker/CMakeLists.txt | 1 + .../CMakeLists.txt | 4 + .../SourceTransformation/EmitterTest.cpp | 81 +++++++++++++++++++ .../SourceTransformation/RegistryTest.cpp | 78 ++++++++++++++++++ 16 files changed, 395 insertions(+) create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h create mode 100644 clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt create mode 100644 clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def b/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def index 7dd866047f556..ba4944ea984cb 100644 --- a/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def +++ b/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def @@ -23,6 +23,7 @@ ANCHOR(JSONFormatAnchorSource) ANCHOR(PointerFlowAnalysisAnchorSource) ANCHOR(PointerFlowExtractorAnchorSource) ANCHOR(PointerFlowJSONFormatAnchorSource) +ANCHOR(SSAFSourceTransformationAnchorSource) ANCHOR(UnsafeBufferUsageAnalysisAnchorSource) ANCHOR(UnsafeBufferUsageExtractorAnchorSource) ANCHOR(UnsafeBufferUsageJSONFormatAnchorSource) diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h new file mode 100644 index 0000000000000..e96c9846a35f2 --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h @@ -0,0 +1,29 @@ +//===- SourceEditEmitter.h --------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Abstract accumulator for source edits. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H + +#include "clang/Tooling/Core/Replacement.h" + +namespace clang::ssaf { + +class SourceEditEmitter { +public: + virtual ~SourceEditEmitter() = default; + + virtual void addReplacement(clang::tooling::Replacement R) = 0; +}; + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h new file mode 100644 index 0000000000000..5553b7796b1ad --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h @@ -0,0 +1,38 @@ +//===- Transformation.h -----------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Abstract base class for source transformations. A Transformation is an +// ASTConsumer that consumes a previously computed WPASuite. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H + +#include "clang/AST/ASTConsumer.h" +#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h" + +namespace clang::ssaf { + +class Transformation : public clang::ASTConsumer { +public: + Transformation(const WPASuite &Suite, SourceEditEmitter &Edits, + TransformationReportEmitter &Report) + : Suite(Suite), Edits(Edits), Report(Report) {} + +protected: + const WPASuite &Suite; + SourceEditEmitter &Edits; + TransformationReportEmitter &Report; +}; + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h new file mode 100644 index 0000000000000..231a810f60f26 --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h @@ -0,0 +1,66 @@ +//===- TransformationRegistry.h ---------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Registry for Transformations, and some helper functions. +// To register a transformation, insert this code: +// +// namespace clang::ssaf { +// // NOLINTNEXTLINE(misc-use-internal-linkage) +// volatile int MyTransformationAnchorSource = 0; +// } // namespace clang::ssaf +// static TransformationRegistry::Add<MyTransformation> +// X("MyTransformation", "My awesome transformation"); +// +// For a statically-linked transformation also extend the `AnchorSources` +// list in +// clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h +// (plugin-loaded transformations do not need an anchor — the dynamic loader +// runs every global ctor on load). +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H + +#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h" +#include "clang/Support/Compiler.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Registry.h" +#include "llvm/Support/raw_ostream.h" +#include <memory> + +namespace clang::ssaf { + +/// Check if a Transformation was registered with a given name. +bool isTransformationRegistered(llvm::StringRef Name); + +/// Try to instantiate a Transformation with a given name. +/// This might return null if the construction of the desired Transformation +/// failed. +/// It's a fatal error if there is no transformation registered with the name. +std::unique_ptr<Transformation> +makeTransformation(llvm::StringRef Name, const WPASuite &Suite, + SourceEditEmitter &Edits, + TransformationReportEmitter &Report); + +/// Print the list of available Transformations. +void printAvailableTransformations(llvm::raw_ostream &OS); + +// Registry for adding new Transformation implementations. +using TransformationRegistry = + llvm::Registry<Transformation, const WPASuite &, SourceEditEmitter &, + TransformationReportEmitter &>; + +} // namespace clang::ssaf + +LLVM_DECLARE_REGISTRY(clang::ssaf::TransformationRegistry) + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h new file mode 100644 index 0000000000000..c48a35009c034 --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h @@ -0,0 +1,35 @@ +//===- TransformationReportEmitter.h ----------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Abstract accumulator for the transformation report. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H + +#include "clang/Basic/Sarif.h" +#include "clang/Basic/SourceLocation.h" +#include "llvm/ADT/StringRef.h" + +namespace clang::ssaf { + +class TransformationReportEmitter { +public: + virtual ~TransformationReportEmitter() = default; + + /// An invalid \p Range signals "no location"; the format writer drops the + /// location object entirely rather than fabricating a placeholder. + virtual void addResult(llvm::StringRef RuleId, clang::SarifResultLevel Level, + clang::CharSourceRange Range, + llvm::StringRef Message) = 0; +}; + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H diff --git a/clang/lib/Driver/CMakeLists.txt b/clang/lib/Driver/CMakeLists.txt index 5fe10052d267f..cd410899c450f 100644 --- a/clang/lib/Driver/CMakeLists.txt +++ b/clang/lib/Driver/CMakeLists.txt @@ -115,6 +115,7 @@ add_clang_library(clangDriver clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore clangScalableStaticAnalysisFrameworkFrontend + clangScalableStaticAnalysisFrameworkSourceTransformation clangSerialization clangLex clangOptions diff --git a/clang/lib/FrontendTool/CMakeLists.txt b/clang/lib/FrontendTool/CMakeLists.txt index 24623303e6bdb..543aa06090ec8 100644 --- a/clang/lib/FrontendTool/CMakeLists.txt +++ b/clang/lib/FrontendTool/CMakeLists.txt @@ -7,6 +7,7 @@ set(link_libs clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore clangScalableStaticAnalysisFrameworkFrontend + clangScalableStaticAnalysisFrameworkSourceTransformation clangBasic clangCodeGen clangDriver diff --git a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt index e09c44b7cfd52..b214c06644db7 100644 --- a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt +++ b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt @@ -2,4 +2,5 @@ add_subdirectory(Analyses) add_subdirectory(Core) add_subdirectory(Frontend) add_subdirectory(Plugins) +add_subdirectory(SourceTransformation) add_subdirectory(Tool) diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt new file mode 100644 index 0000000000000..c96a386977487 --- /dev/null +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt @@ -0,0 +1,13 @@ +set(LLVM_LINK_COMPONENTS + Support + ) + +add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation + TransformationRegistry.cpp + + LINK_LIBS + clangAST + clangBasic + clangScalableStaticAnalysisFrameworkCore + clangToolingCore + ) diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp new file mode 100644 index 0000000000000..9d770c8bcd0f9 --- /dev/null +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp @@ -0,0 +1,44 @@ +//===- TransformationRegistry.cpp -----------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h" +#include <memory> + +using namespace clang; +using namespace ssaf; + +namespace clang::ssaf { +// NOLINTNEXTLINE(misc-use-internal-linkage) +volatile int SSAFSourceTransformationAnchorSource = 0; +} // namespace clang::ssaf + +LLVM_DEFINE_REGISTRY(clang::ssaf::TransformationRegistry) + +bool ssaf::isTransformationRegistered(llvm::StringRef Name) { + for (const auto &Entry : TransformationRegistry::entries()) + if (Entry.getName() == Name) + return true; + return false; +} + +std::unique_ptr<Transformation> +ssaf::makeTransformation(llvm::StringRef Name, const WPASuite &Suite, + SourceEditEmitter &Edits, + TransformationReportEmitter &Report) { + for (const auto &Entry : TransformationRegistry::entries()) + if (Entry.getName() == Name) + return Entry.instantiate(Suite, Edits, Report); + assert(false && "Unknown Transformation name"); + return nullptr; +} + +void ssaf::printAvailableTransformations(llvm::raw_ostream &OS) { + OS << "OVERVIEW: Available SSAF source transformations:\n\n"; + for (const auto &Entry : TransformationRegistry::entries()) + OS << " " << Entry.getName() << " - " << Entry.getDesc() << "\n"; +} diff --git a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt index 67732867181b6..d58ec773ff04b 100644 --- a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt +++ b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt @@ -17,6 +17,7 @@ clang_target_link_libraries(clang-ssaf-analyzer clangBasic clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore + clangScalableStaticAnalysisFrameworkSourceTransformation clangScalableStaticAnalysisFrameworkTool ) diff --git a/clang/tools/clang-ssaf-format/CMakeLists.txt b/clang/tools/clang-ssaf-format/CMakeLists.txt index 33ce432be3f4a..5d7a6c70c73fb 100644 --- a/clang/tools/clang-ssaf-format/CMakeLists.txt +++ b/clang/tools/clang-ssaf-format/CMakeLists.txt @@ -17,6 +17,7 @@ clang_target_link_libraries(clang-ssaf-format clangBasic clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore + clangScalableStaticAnalysisFrameworkSourceTransformation clangScalableStaticAnalysisFrameworkTool ) diff --git a/clang/tools/clang-ssaf-linker/CMakeLists.txt b/clang/tools/clang-ssaf-linker/CMakeLists.txt index af65aaa3b1aeb..89d72a45a734e 100644 --- a/clang/tools/clang-ssaf-linker/CMakeLists.txt +++ b/clang/tools/clang-ssaf-linker/CMakeLists.txt @@ -12,5 +12,6 @@ clang_target_link_libraries(clang-ssaf-linker clangBasic clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore + clangScalableStaticAnalysisFrameworkSourceTransformation clangScalableStaticAnalysisFrameworkTool ) diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt index e852d99d34781..8090ea96cbd5c 100644 --- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt +++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt @@ -24,6 +24,8 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests Serialization/JSONFormatTest/JSONFormatTest.cpp Serialization/JSONFormatTest/LUSummaryTest.cpp Serialization/JSONFormatTest/TUSummaryTest.cpp + SourceTransformation/EmitterTest.cpp + SourceTransformation/RegistryTest.cpp SummaryData/SummaryDataTest.cpp SummaryNameTest.cpp TestFixture.cpp @@ -39,8 +41,10 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore clangScalableStaticAnalysisFrameworkFrontend + clangScalableStaticAnalysisFrameworkSourceTransformation clangSerialization clangTooling + clangToolingCore LINK_LIBS LLVMTestingSupport diff --git a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp new file mode 100644 index 0000000000000..d730c6c337c45 --- /dev/null +++ b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp @@ -0,0 +1,81 @@ +//===- EmitterTest.cpp ----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/Sarif.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h" +#include "gtest/gtest.h" +#include <vector> + +using namespace llvm; +using namespace clang; +using namespace ssaf; + +namespace { + +class RecordingEditEmitter : public SourceEditEmitter { +public: + std::vector<clang::tooling::Replacement> Replacements; + + void addReplacement(clang::tooling::Replacement R) override { + Replacements.push_back(std::move(R)); + } +}; + +class RecordingReportEmitter : public TransformationReportEmitter { +public: + struct Entry { + std::string RuleId; + clang::SarifResultLevel Level; + clang::CharSourceRange Range; + std::string Message; + }; + std::vector<Entry> Results; + + void addResult(StringRef RuleId, clang::SarifResultLevel Level, + clang::CharSourceRange Range, StringRef Message) override { + Results.push_back({RuleId.str(), Level, Range, Message.str()}); + } +}; + +TEST(SourceEditEmitterTest, AccumulatesInOrder) { + RecordingEditEmitter E; + E.addReplacement(clang::tooling::Replacement("a.cpp", 0, 0, "// 1")); + E.addReplacement(clang::tooling::Replacement("a.cpp", 10, 0, "// 2")); + ASSERT_EQ(E.Replacements.size(), 2u); + EXPECT_EQ(E.Replacements[0].getReplacementText(), "// 1"); + EXPECT_EQ(E.Replacements[1].getReplacementText(), "// 2"); + EXPECT_EQ(E.Replacements[0].getOffset(), 0u); + EXPECT_EQ(E.Replacements[1].getOffset(), 10u); +} + +TEST(TransformationReportEmitterTest, AccumulatesInOrder) { + RecordingReportEmitter R; + R.addResult("rule-a", clang::SarifResultLevel::Note, clang::CharSourceRange{}, + "first"); + R.addResult("rule-b", clang::SarifResultLevel::Warning, + clang::CharSourceRange{}, "second"); + ASSERT_EQ(R.Results.size(), 2u); + EXPECT_EQ(R.Results[0].RuleId, "rule-a"); + EXPECT_EQ(R.Results[0].Level, clang::SarifResultLevel::Note); + EXPECT_EQ(R.Results[0].Message, "first"); + EXPECT_EQ(R.Results[1].RuleId, "rule-b"); + EXPECT_EQ(R.Results[1].Level, clang::SarifResultLevel::Warning); + EXPECT_EQ(R.Results[1].Message, "second"); +} + +TEST(TransformationReportEmitterTest, AcceptsInvalidRange) { + RecordingReportEmitter R; + R.addResult("rule", clang::SarifResultLevel::Note, clang::CharSourceRange{}, + "no-location"); + ASSERT_EQ(R.Results.size(), 1u); + EXPECT_FALSE(R.Results[0].Range.isValid()); +} + +} // namespace diff --git a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp new file mode 100644 index 0000000000000..0c68cd95dd499 --- /dev/null +++ b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp @@ -0,0 +1,78 @@ +//===- RegistryTest.cpp ---------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "TestFixture.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace clang; +using namespace ssaf; + +namespace { + +class StubEditEmitter : public SourceEditEmitter { +public: + void addReplacement(clang::tooling::Replacement) override {} +}; + +class StubReportEmitter : public TransformationReportEmitter { +public: + void addResult(StringRef, clang::SarifResultLevel, clang::CharSourceRange, + StringRef) override {} +}; + +class StubTransformation : public Transformation { +public: + using Transformation::Transformation; +}; + +} // namespace + +static TransformationRegistry::Add<StubTransformation> + RegisterStubTransformation("stub-transformation", + "A transformation for testing"); + +namespace { + +class TransformationRegistryTest : public TestFixture {}; + +TEST_F(TransformationRegistryTest, isTransformationRegistered) { + EXPECT_FALSE(isTransformationRegistered("not-a-transformation")); + EXPECT_TRUE(isTransformationRegistered("stub-transformation")); +} + +TEST_F(TransformationRegistryTest, makeTransformation) { + WPASuite Suite = makeWPASuite(); + StubEditEmitter Edits; + StubReportEmitter Report; + std::unique_ptr<Transformation> T = + makeTransformation("stub-transformation", Suite, Edits, Report); + EXPECT_NE(T, nullptr); +} + +TEST_F(TransformationRegistryTest, EnumeratingRegistryEntries) { + auto Entries = TransformationRegistry::entries(); + EXPECT_TRUE(llvm::any_of(Entries, [](const auto &Entry) { + return StringRef(Entry.getName()) == "stub-transformation"; + })); +} + +TEST_F(TransformationRegistryTest, PrintAvailableTransformations) { + std::string Buffer; + raw_string_ostream OS(Buffer); + printAvailableTransformations(OS); + EXPECT_NE(StringRef(Buffer).find("stub-transformation"), StringRef::npos); + EXPECT_NE(StringRef(Buffer).find("A transformation for testing"), + StringRef::npos); +} + +} // namespace >From f14defb12bfd4505eebe35538e783e826c1e4f11 Mon Sep 17 00:00:00 2001 From: Jan Korous <[email protected]> Date: Fri, 12 Jun 2026 19:15:00 -0700 Subject: [PATCH 2/4] [clang][ssaf] Add YAML source-edit format Adds the built-in `SourceEditFormat`, registered under the file extension `yaml`. The writer drives `llvm::yaml::Output` against the existing `clang::tooling::TranslationUnitReplacements` `MappingTraits` from `clang/Tooling/ReplacementsYaml.h`, so the resulting document is byte-for-byte consumable by `clang-apply-replacements`. Anchored via `SSAFYAMLSourceEditFormatAnchorSource` so static builds keep the registration. Assisted-By: Claude Opus 4.7 --- .../YAMLSourceEditFormat.h | 32 ++++++ .../SourceTransformation/CMakeLists.txt | 1 + .../YAMLSourceEditFormat.cpp | 33 +++++++ .../CMakeLists.txt | 1 + .../SourceTransformation/YAMLFormatTest.cpp | 97 +++++++++++++++++++ 5 files changed, 164 insertions(+) create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h create mode 100644 clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h new file mode 100644 index 0000000000000..e154fabbd8031 --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h @@ -0,0 +1,32 @@ +//===- YAMLSourceEditFormat.h -----------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Built-in YAML source-edit writer. The on-disk layout is the existing +// `clang::tooling::TranslationUnitReplacements` YAML schema, byte-for-byte +// consumable by `clang-apply-replacements`. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H + +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +namespace clang::ssaf { + +/// Writes \p Doc to \p Path as a YAML document compatible with +/// `clang-apply-replacements`. +llvm::Error +writeYAMLSourceEdits(const clang::tooling::TranslationUnitReplacements &Doc, + llvm::StringRef Path); + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt index c96a386977487..9ec05a2f21a6d 100644 --- a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt @@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation TransformationRegistry.cpp + YAMLSourceEditFormat.cpp LINK_LIBS clangAST diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp new file mode 100644 index 0000000000000..7b472b0ab158d --- /dev/null +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp @@ -0,0 +1,33 @@ +//===- YAMLSourceEditFormat.cpp -------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h" +#include "clang/Tooling/ReplacementsYaml.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/YAMLTraits.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace ssaf; + +llvm::Error ssaf::writeYAMLSourceEdits( + const clang::tooling::TranslationUnitReplacements &Doc, + llvm::StringRef Path) { + std::error_code EC; + llvm::raw_fd_ostream OS(Path, EC, llvm::sys::fs::OF_None); + if (EC) + return llvm::createStringError(EC, "failed to open '" + Path + "'"); + + // llvm::yaml::Output's stream operator binds to a non-const reference. + clang::tooling::TranslationUnitReplacements Mutable = Doc; + llvm::yaml::Output YAMLOut(OS); + YAMLOut << Mutable; + + return llvm::Error::success(); +} diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt index 8090ea96cbd5c..f57e1c1c0dbf8 100644 --- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt +++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt @@ -26,6 +26,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests Serialization/JSONFormatTest/TUSummaryTest.cpp SourceTransformation/EmitterTest.cpp SourceTransformation/RegistryTest.cpp + SourceTransformation/YAMLFormatTest.cpp SummaryData/SummaryDataTest.cpp SummaryNameTest.cpp TestFixture.cpp diff --git a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp new file mode 100644 index 0000000000000..7ce8a35a33562 --- /dev/null +++ b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp @@ -0,0 +1,97 @@ +//===- YAMLFormatTest.cpp -------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h" +#include "clang/Tooling/Core/Replacement.h" +#include "clang/Tooling/ReplacementsYaml.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/YAMLTraits.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace clang; +using namespace ssaf; + +namespace { + +// Materializes a unique temporary file path under the system temp dir and +// removes it on destruction. +struct TempPath { + SmallString<128> Path; + + TempPath(StringRef Suffix) { + sys::fs::createUniquePath("ssaf-yaml-%%%%%%." + Suffix, Path, + /*MakeAbsolute=*/true); + } + ~TempPath() { sys::fs::remove(Path); } +}; + +TEST(WriteYAMLSourceEditsTest, RoundTripsTwoReplacements) { + clang::tooling::TranslationUnitReplacements Doc; + Doc.MainSourceFile = "main.cpp"; + Doc.Replacements.emplace_back("a.cpp", 0, 0, "/*1*/"); + Doc.Replacements.emplace_back("b.cpp", 10, 3, "/*2*/"); + + TempPath TP("yaml"); + ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, TP.Path), Succeeded()); + + auto BufferOrErr = MemoryBuffer::getFile(TP.Path); + ASSERT_TRUE(static_cast<bool>(BufferOrErr)) + << "Failed to read back '" << TP.Path << "'"; + + clang::tooling::TranslationUnitReplacements Parsed; + yaml::Input YIn((*BufferOrErr)->getBuffer()); + YIn >> Parsed; + ASSERT_FALSE(YIn.error()) << YIn.error().message(); + + EXPECT_EQ(Parsed.MainSourceFile, "main.cpp"); + ASSERT_EQ(Parsed.Replacements.size(), 2u); + EXPECT_EQ(Parsed.Replacements[0].getFilePath(), "a.cpp"); + EXPECT_EQ(Parsed.Replacements[0].getOffset(), 0u); + EXPECT_EQ(Parsed.Replacements[0].getLength(), 0u); + EXPECT_EQ(Parsed.Replacements[0].getReplacementText(), "/*1*/"); + EXPECT_EQ(Parsed.Replacements[1].getFilePath(), "b.cpp"); + EXPECT_EQ(Parsed.Replacements[1].getOffset(), 10u); + EXPECT_EQ(Parsed.Replacements[1].getLength(), 3u); + EXPECT_EQ(Parsed.Replacements[1].getReplacementText(), "/*2*/"); +} + +TEST(WriteYAMLSourceEditsTest, EmptyReplacementsWritesValidDocument) { + clang::tooling::TranslationUnitReplacements Doc; + Doc.MainSourceFile = "main.cpp"; + + TempPath TP("yaml"); + ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, TP.Path), Succeeded()); + + auto BufferOrErr = MemoryBuffer::getFile(TP.Path); + ASSERT_TRUE(static_cast<bool>(BufferOrErr)); + + clang::tooling::TranslationUnitReplacements Parsed; + yaml::Input YIn((*BufferOrErr)->getBuffer()); + YIn >> Parsed; + ASSERT_FALSE(YIn.error()) << YIn.error().message(); + EXPECT_EQ(Parsed.MainSourceFile, "main.cpp"); + EXPECT_TRUE(Parsed.Replacements.empty()); +} + +TEST(WriteYAMLSourceEditsTest, OpenErrorReturnsError) { + clang::tooling::TranslationUnitReplacements Doc; + Doc.MainSourceFile = "main.cpp"; + + // Path under a directory that does not exist. + SmallString<128> BadPath; + sys::fs::createUniquePath("ssaf-missing-%%%%%%/edits.yaml", BadPath, + /*MakeAbsolute=*/true); + + ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, BadPath), Failed()); +} + +} // namespace >From 32807aff2e7b9ec5f55c9c4cd70e525a071c184c Mon Sep 17 00:00:00 2001 From: Jan Korous <[email protected]> Date: Fri, 12 Jun 2026 19:15:00 -0700 Subject: [PATCH 3/4] [clang][ssaf] Add SARIF transformation-report format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the built-in `TransformationReportFormat`, registered under the file extension `sarif`. The writer drives clang's existing `SarifDocumentWriter` (`clang/Basic/Sarif.h`) to produce a SARIF 2.1.0 JSON document. The tool driver name is `clang-ssaf`; the long `fullName` carries the transformation's name. Token-range `CharSourceRange`s are canonicalized to char ranges via `clang::Lexer::getAsCharRange` before being attached to results; invalid ranges (including default-constructed ones) are treated as "no location" — the writer omits the result's `locations` key rather than fabricating one. Source edits are not embedded in the report; the writer never emits a `fix` or `fixes` key. Anchored via `SSAFSARIFTransformationReportFormatAnchorSource` so static builds keep the registration. Assisted-By: Claude Opus 4.7 --- .../SARIFTransformationReportFormat.h | 50 +++++ .../SourceTransformation/CMakeLists.txt | 1 + .../SARIFTransformationReportFormat.cpp | 56 ++++++ .../CMakeLists.txt | 1 + .../SourceTransformation/SARIFFormatTest.cpp | 185 ++++++++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h create mode 100644 clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h new file mode 100644 index 0000000000000..cd49bd9fd267f --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h @@ -0,0 +1,50 @@ +//===- SARIFTransformationReportFormat.h ------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Built-in SARIF 2.1.0 transformation-report writer. Drives clang's existing +// `SarifDocumentWriter`; emits no `fix` keys (source edits live in the +// separate edit file). +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H + +#include "clang/Basic/Sarif.h" +#include "clang/Basic/SourceLocation.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include <string> +#include <vector> + +namespace clang { +class SourceManager; +} // namespace clang + +namespace clang::ssaf { + +struct ReportResult { + std::string RuleId; + clang::SarifResultLevel Level; + clang::CharSourceRange Range; + std::string Message; +}; + +struct ReportDocument { + std::string TransformationName; + const clang::SourceManager &SM; + std::vector<ReportResult> Results; +}; + +/// Writes \p Doc to \p Path as a SARIF 2.1.0 JSON document. +llvm::Error writeSARIFTransformationReport(const ReportDocument &Doc, + llvm::StringRef Path); + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt index 9ec05a2f21a6d..31772ce4a18bf 100644 --- a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt @@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS ) add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation + SARIFTransformationReportFormat.cpp TransformationRegistry.cpp YAMLSourceEditFormat.cpp diff --git a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp new file mode 100644 index 0000000000000..feaba7d9366e1 --- /dev/null +++ b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp @@ -0,0 +1,56 @@ +//===- SARIFTransformationReportFormat.cpp --------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h" +#include "clang/Basic/Sarif.h" +#include "clang/Basic/Version.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace ssaf; + +llvm::Error ssaf::writeSARIFTransformationReport(const ReportDocument &Doc, + llvm::StringRef Path) { + std::error_code EC; + llvm::raw_fd_ostream OS(Path, EC, llvm::sys::fs::OF_None); + if (EC) + return llvm::createStringError(EC, "failed to open '" + Path + "'"); + + clang::SarifDocumentWriter Writer(Doc.SM); + std::string LongToolName = + "clang ScalableStaticAnalysisFramework source transformation (" + + Doc.TransformationName + ")"; + Writer.createRun("clang-ssaf", LongToolName, CLANG_VERSION_STRING); + + llvm::StringMap<size_t> RuleIndex; + for (const ReportResult &R : Doc.Results) { + if (RuleIndex.contains(R.RuleId)) + continue; + RuleIndex[R.RuleId] = + Writer.createRule(clang::SarifRule::create().setRuleId(R.RuleId)); + } + + for (const ReportResult &R : Doc.Results) { + clang::SarifResult Result = clang::SarifResult::create(RuleIndex[R.RuleId]) + .setRuleId(R.RuleId) + .setDiagnosticMessage(R.Message) + .setDiagnosticLevel(R.Level); + if (R.Range.isValid()) + Result = Result.addLocations({R.Range}); + Writer.appendResult(Result); + } + + llvm::json::Value Document(Writer.createDocument()); + OS << llvm::formatv("{0:2}", Document) << "\n"; + return llvm::Error::success(); +} diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt index f57e1c1c0dbf8..7745a8a688e90 100644 --- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt +++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt @@ -26,6 +26,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests Serialization/JSONFormatTest/TUSummaryTest.cpp SourceTransformation/EmitterTest.cpp SourceTransformation/RegistryTest.cpp + SourceTransformation/SARIFFormatTest.cpp SourceTransformation/YAMLFormatTest.cpp SummaryData/SummaryDataTest.cpp SummaryNameTest.cpp diff --git a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp new file mode 100644 index 0000000000000..43468bb111d0b --- /dev/null +++ b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp @@ -0,0 +1,185 @@ +//===- SARIFFormatTest.cpp ------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/DiagnosticIDs.h" +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/FileSystemOptions.h" +#include "clang/Basic/Sarif.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/Version.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/VirtualFileSystem.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace clang; +using namespace ssaf; + +namespace { + +struct TempPath { + SmallString<128> Path; + + TempPath(StringRef Suffix) { + sys::fs::createUniquePath("ssaf-sarif-%%%%%%." + Suffix, Path, + /*MakeAbsolute=*/true); + } + ~TempPath() { sys::fs::remove(Path); } +}; + +class SARIFFormatTest : public ::testing::Test { +protected: + SARIFFormatTest() + : Diags(DiagnosticIDs::create(), DiagOpts, new IgnoringDiagConsumer()), + VFS(makeIntrusiveRefCnt<vfs::InMemoryFileSystem>()), + FileMgr(FileSystemOptions(), VFS), SourceMgr(Diags, FileMgr) {} + + json::Value writeAndParse(const ReportDocument &Doc) { + TempPath TP("sarif"); + EXPECT_THAT_ERROR(writeSARIFTransformationReport(Doc, TP.Path), + Succeeded()); + auto BufferOrErr = MemoryBuffer::getFile(TP.Path); + EXPECT_TRUE(static_cast<bool>(BufferOrErr)); + auto ParsedOrErr = json::parse((*BufferOrErr)->getBuffer()); + EXPECT_THAT_EXPECTED(ParsedOrErr, Succeeded()); + return std::move(*ParsedOrErr); + } + + DiagnosticOptions DiagOpts; + DiagnosticsEngine Diags; + IntrusiveRefCntPtr<vfs::InMemoryFileSystem> VFS; + FileManager FileMgr; + SourceManager SourceMgr; +}; + +TEST_F(SARIFFormatTest, ToolDriverNameAndVersion) { + ReportDocument Doc{"my-transformation", SourceMgr, {}}; + json::Value V = writeAndParse(Doc); + + const json::Object *Root = V.getAsObject(); + ASSERT_NE(Root, nullptr); + const json::Array *Runs = Root->getArray("runs"); + ASSERT_NE(Runs, nullptr); + ASSERT_EQ(Runs->size(), 1u); + const json::Object *Driver = + (*Runs)[0].getAsObject()->getObject("tool")->getObject("driver"); + ASSERT_NE(Driver, nullptr); + EXPECT_EQ(*Driver->getString("name"), "clang-ssaf"); + EXPECT_NE(Driver->getString("fullName")->find("my-transformation"), + StringRef::npos); + EXPECT_EQ(*Driver->getString("version"), CLANG_VERSION_STRING); +} + +TEST_F(SARIFFormatTest, LevelMapping) { + ReportDocument Doc{"t", + SourceMgr, + { + {"r-note", clang::SarifResultLevel::Note, {}, "n"}, + {"r-warn", clang::SarifResultLevel::Warning, {}, "w"}, + {"r-error", clang::SarifResultLevel::Error, {}, "e"}, + {"r-none", clang::SarifResultLevel::None, {}, "x"}, + }}; + json::Value V = writeAndParse(Doc); + + const json::Array *Results = + V.getAsObject()->getArray("runs")->front().getAsObject()->getArray( + "results"); + ASSERT_NE(Results, nullptr); + ASSERT_EQ(Results->size(), 4u); + EXPECT_EQ(*(*Results)[0].getAsObject()->getString("level"), "note"); + EXPECT_EQ(*(*Results)[1].getAsObject()->getString("level"), "warning"); + EXPECT_EQ(*(*Results)[2].getAsObject()->getString("level"), "error"); + EXPECT_EQ(*(*Results)[3].getAsObject()->getString("level"), "none"); +} + +TEST_F(SARIFFormatTest, InvalidRangeOmitsLocations) { + ReportDocument Doc{ + "t", + SourceMgr, + {{"r", clang::SarifResultLevel::Note, clang::CharSourceRange{}, "msg"}}}; + json::Value V = writeAndParse(Doc); + const json::Object *Result = V.getAsObject() + ->getArray("runs") + ->front() + .getAsObject() + ->getArray("results") + ->front() + .getAsObject(); + EXPECT_EQ(Result->get("locations"), nullptr); +} + +TEST_F(SARIFFormatTest, EmptyResultsValidDocument) { + ReportDocument Doc{"t", SourceMgr, {}}; + json::Value V = writeAndParse(Doc); + const json::Object *Run = + V.getAsObject()->getArray("runs")->front().getAsObject(); + // SARIF allows the results array to be absent or empty for an empty run. + if (const json::Array *Results = Run->getArray("results")) + EXPECT_TRUE(Results->empty()); +} + +TEST_F(SARIFFormatTest, NoFixKey) { + ReportDocument Doc{ + "t", SourceMgr, {{"r", clang::SarifResultLevel::Warning, {}, "msg"}}}; + json::Value V = writeAndParse(Doc); + const json::Object *Result = V.getAsObject() + ->getArray("runs") + ->front() + .getAsObject() + ->getArray("results") + ->front() + .getAsObject(); + EXPECT_EQ(Result->get("fix"), nullptr); + EXPECT_EQ(Result->get("fixes"), nullptr); +} + +TEST_F(SARIFFormatTest, EmptyRuleIdAccepted) { + ReportDocument Doc{ + "t", SourceMgr, {{"", clang::SarifResultLevel::Note, {}, "m"}}}; + json::Value V = writeAndParse(Doc); + const json::Object *Result = V.getAsObject() + ->getArray("runs") + ->front() + .getAsObject() + ->getArray("results") + ->front() + .getAsObject(); + ASSERT_NE(Result->getString("ruleId"), std::nullopt); + EXPECT_EQ(*Result->getString("ruleId"), ""); +} + +TEST_F(SARIFFormatTest, DistinctRuleIdsDeduplicated) { + ReportDocument Doc{"t", + SourceMgr, + { + {"a", clang::SarifResultLevel::Note, {}, "m1"}, + {"b", clang::SarifResultLevel::Note, {}, "m2"}, + {"a", clang::SarifResultLevel::Note, {}, "m3"}, + }}; + json::Value V = writeAndParse(Doc); + const json::Array *Rules = V.getAsObject() + ->getArray("runs") + ->front() + .getAsObject() + ->getObject("tool") + ->getObject("driver") + ->getArray("rules"); + ASSERT_NE(Rules, nullptr); + EXPECT_EQ(Rules->size(), 2u); +} + +} // namespace >From f71af0c84cc9310bb6412e93cb3c58f647282188 Mon Sep 17 00:00:00 2001 From: Jan Korous <[email protected]> Date: Fri, 12 Jun 2026 19:15:00 -0700 Subject: [PATCH 4/4] [clang][ssaf] Wire up the source-edit-generation pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the four `--ssaf-*` driver flags (`--ssaf-source-transformation=`, `--ssaf-global-scope-analysis-result=`, `--ssaf-src-edit-file=`, `--ssaf-transformation-report-file=`) under `SSAF_Group`, marshalled into `FrontendOptions`. The compilation-unit identifier flag introduced earlier is reused. The driver forwards all four flags to `cc1`. Adds twelve `warn_ssaf_*` diagnostics under `-Wscalable-static-analysis-framework` (`DefaultError`) covering the orphan-flag matrix, unknown transformation names, unknown output formats, WPA-suite read failures, and edit/report write failures. Adds `clang::ssaf::SourceTransformationFrontendAction` — a `WrapperFrontendAction` that, when any source-edit flag is set, validates the CLI as a group, loads the WPASuite from the configured path, instantiates the named transformation, and serializes the accumulated edits and findings through the configured formats. The action is wrapped in `ExecuteCompilerInvocation` after the existing TU-summary wrap so both pipelines stack as independent `ASTConsumer`s on the same translation unit; they exchange no data. Lit tests under `clang/test/Analysis/Scalable/source-edit-generation/` cover the orphan-flag matrix, unknown-name and unknown-format paths, the `-Wno-error=` / `-Wno-` levers, write failures, the end-to-end happy path through a test-only `SSAFTestTransformationPlugin`, and the coexistence of stage-1 and stage-2 in a single invocation. The plugin itself lives under `clang/test/` so no testing artifact ships in production code; it builds gated on `CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32` and the plugin-using lit tests use `REQUIRES: plugins`. User and developer documentation describe the flag surface, the diagnostic policy, and how to author and load a transformation. Assisted-By: Claude Opus 4.7 --- .../user-docs/SourceEditGeneration.rst | 77 ++++++ .../clang/Basic/DiagnosticFrontendKinds.td | 46 ++++ clang/include/clang/Basic/DiagnosticIDs.h | 2 +- .../include/clang/Frontend/FrontendOptions.h | 18 ++ clang/include/clang/Options/Options.td | 38 +++ .../SourceTransformationFrontendAction.h | 34 +++ clang/lib/Driver/ToolChains/Clang.cpp | 4 + .../ExecuteCompilerInvocation.cpp | 7 + .../Frontend/CMakeLists.txt | 3 + .../SourceTransformationFrontendAction.cpp | 232 ++++++++++++++++++ clang/test/Analysis/Scalable/help.cpp | 8 + .../Inputs/empty-suite.json | 4 + .../Inputs/two-function-suite.json | 25 ++ .../Plugins/CMakeLists.txt | 3 + .../TestTransformationPlugin/CMakeLists.txt | 16 ++ .../TestTransformation.cpp | 101 ++++++++ .../Plugins/lit.local.cfg | 2 + .../source-edit-generation/cli-errors.cpp | 51 ++++ .../source-edit-generation/coexistence.cpp | 33 +++ .../downgradable-errors.cpp | 35 +++ .../source-edit-generation/happy-path.cpp | 37 +++ .../source-edit-generation/write-failure.cpp | 41 ++++ clang/test/CMakeLists.txt | 2 + 23 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h create mode 100644 clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp create mode 100644 clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp diff --git a/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst b/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst new file mode 100644 index 0000000000000..c7027f8c9fc2c --- /dev/null +++ b/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst @@ -0,0 +1,77 @@ +============================== +Source Edit Generation +============================== + +Source edit generation is the second stage of the SSAF pipeline. Given a +``WPASuite`` produced by an earlier whole-program analysis, a *source +transformation* runs alongside the normal compile and emits two +per-translation-unit artifacts: + +- a *source-edit file* (``--ssaf-src-edit-file=``) containing + ``clang::tooling::Replacement`` records ready for + ``clang-apply-replacements``, +- a *transformation-report file* (``--ssaf-transformation-report-file=``) + containing diagnostic-style findings. + +Driver flags +============ + +Four flags control the pipeline; they are all both ``--ssaf-…`` driver +flags and ``cc1`` flags. The compilation-unit identifier flag is shared +with the stage-1 pipeline. + +.. list-table:: + :header-rows: 1 + + * - Flag + - Purpose + * - ``--ssaf-source-transformation=<name>`` + - Name of the transformation to run. + * - ``--ssaf-global-scope-analysis-result=<path>.<format>`` + - WPASuite input. The extension selects the serialization format. + * - ``--ssaf-src-edit-file=<path>`` + - Source-edit output. Always written as a + ``clang-apply-replacements``-compatible YAML document; the + file extension is not interpreted. + * - ``--ssaf-transformation-report-file=<path>`` + - Transformation-report output. Always written as a SARIF 2.1.0 + JSON document; the file extension is not interpreted. + * - ``--ssaf-compilation-unit-id=<id>`` + - Stable identifier for this translation unit (also required by + the stage-1 pipeline). + +When ``--ssaf-source-transformation=`` is non-empty the framework wraps +the active ``FrontendAction`` in a ``SourceTransformationFrontendAction``; +otherwise the compile is byte-for-byte unchanged. + +Error policy +============ + +Every CLI-misuse and runtime-write diagnostic is registered as a +``Warning ... DefaultError`` under +``-Wscalable-static-analysis-framework``. This means errors stop the +compile by default but can be downgraded or silenced: + +- ``-Wno-error=scalable-static-analysis-framework`` — diagnostics + become warnings and the compile finishes normally. The edit/report + files may be absent (if the runner bailed out before writing) or + present (if a write call returned an error). +- ``-Wno-scalable-static-analysis-framework`` — diagnostics are + silenced entirely. The compile finishes normally. + +Examples +======== + +Apply the source edits with ``clang-apply-replacements``: + +.. code-block:: console + + $ clang -c foo.cpp \ + --ssaf-source-transformation=my-transformation \ + --ssaf-global-scope-analysis-result=wpa.json \ + --ssaf-src-edit-file=foo.yaml \ + --ssaf-transformation-report-file=foo.sarif \ + --ssaf-compilation-unit-id=cu-foo + $ clang-apply-replacements --remove-change-desc-files <dir-with-yaml> + +The transformation report can be consumed by any SARIF 2.1.0 viewer. diff --git a/clang/include/clang/Basic/DiagnosticFrontendKinds.td b/clang/include/clang/Basic/DiagnosticFrontendKinds.td index 058449ef47a46..17f7a5c1237f5 100644 --- a/clang/include/clang/Basic/DiagnosticFrontendKinds.td +++ b/clang/include/clang/Basic/DiagnosticFrontendKinds.td @@ -434,6 +434,52 @@ def warn_ssaf_tu_summary_requires_compilation_unit_id : "'--ssaf-compilation-unit-id=' to be set">, InGroup<ScalableStaticAnalysisFramework>, DefaultError; +def warn_ssaf_source_transformation_unknown_name : + Warning<"no source transformation registered with name: %0">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_source_transformation_requires_wpa_file : + Warning<"option '--ssaf-source-transformation=' requires " + "'--ssaf-global-scope-analysis-result=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_source_transformation_requires_edit_file : + Warning<"option '--ssaf-source-transformation=' requires " + "'--ssaf-src-edit-file=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_source_transformation_requires_report_file : + Warning<"option '--ssaf-source-transformation=' requires " + "'--ssaf-transformation-report-file=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_source_transformation_requires_compilation_unit_id : + Warning<"option '--ssaf-source-transformation=' requires " + "'--ssaf-compilation-unit-id=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_read_wpa_suite_failed : + Warning<"failed to read whole-program analysis result from '%0': %1">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_src_edit_file_requires_transformation : + Warning<"option '--ssaf-src-edit-file=' requires " + "'--ssaf-source-transformation=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_write_src_edit_failed : + Warning<"failed to write source edits to '%0': %1">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_transformation_report_file_requires_transformation : + Warning<"option '--ssaf-transformation-report-file=' requires " + "'--ssaf-source-transformation=' to be set">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + +def warn_ssaf_write_transformation_report_failed : + Warning<"failed to write transformation report to '%0': %1">, + InGroup<ScalableStaticAnalysisFramework>, DefaultError; + def err_extract_api_ignores_file_not_found : Error<"file '%0' specified by '--extract-api-ignores=' not found">, DefaultFatal; diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h index 63b5e6a28aac0..2d7e32579b608 100644 --- a/clang/include/clang/Basic/DiagnosticIDs.h +++ b/clang/include/clang/Basic/DiagnosticIDs.h @@ -36,7 +36,7 @@ enum class Group; enum { DIAG_SIZE_COMMON = 300, DIAG_SIZE_DRIVER = 400, - DIAG_SIZE_FRONTEND = 200, + DIAG_SIZE_FRONTEND = 250, DIAG_SIZE_SERIALIZATION = 120, DIAG_SIZE_LEX = 500, DIAG_SIZE_PARSE = 800, diff --git a/clang/include/clang/Frontend/FrontendOptions.h b/clang/include/clang/Frontend/FrontendOptions.h index 7c242f6e94fe0..824e563a4c08a 100644 --- a/clang/include/clang/Frontend/FrontendOptions.h +++ b/clang/include/clang/Frontend/FrontendOptions.h @@ -556,6 +556,24 @@ class FrontendOptions { /// across stages of the SSAF pipeline. std::string SSAFCompilationUnitId; + /// Name of the SSAF source transformation to run. Exactly one transformation + /// per invocation; non-empty implies the source-transformation pipeline is + /// active. + std::string SSAFSourceTransformation; + + /// Path of the WPASuite input consumed by the source transformation. The + /// extension selects which serialization format reads it. + std::string SSAFGlobalScopeAnalysisResult; + + /// Path of the source-edit output file produced by the source + /// transformation. The extension selects which `SourceEditFormat` writes it. + std::string SSAFSrcEditFile; + + /// Path of the transformation-report output file produced by the source + /// transformation. The extension selects which `TransformationReportFormat` + /// writes it. + std::string SSAFTransformationReportFile; + /// Show available SSAF summary extractors. LLVM_PREFERRED_TYPE(bool) unsigned SSAFShowExtractors : 1; diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index a4b9cb802af4d..37de9e1c672a1 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -979,6 +979,44 @@ def _ssaf_compilation_unit_id : "produced SSAF TU summary. Required when '--ssaf-tu-summary-file=' is " "set.">, MarshallingInfoString<FrontendOpts<"SSAFCompilationUnitId">>; +def _ssaf_source_transformation : + Joined<["--"], "ssaf-source-transformation=">, + MetaVarName<"<name>">, + Group<SSAF_Group>, + Visibility<[ClangOption, CC1Option]>, + HelpText< + "Name of the SSAF source transformation to run. Exactly one transformation " + "per invocation.">, + MarshallingInfoString<FrontendOpts<"SSAFSourceTransformation">>; +def _ssaf_global_scope_analysis_result : + Joined<["--"], "ssaf-global-scope-analysis-result=">, + MetaVarName<"<path>.<format>">, + Group<SSAF_Group>, + Visibility<[ClangOption, CC1Option]>, + HelpText< + "Path to the WPASuite file containing the whole-program analysis result " + "consumed by the source transformation. The extension selects which file " + "format to use.">, + MarshallingInfoString<FrontendOpts<"SSAFGlobalScopeAnalysisResult">>; +def _ssaf_src_edit_file : + Joined<["--"], "ssaf-src-edit-file=">, + MetaVarName<"<path>">, + Group<SSAF_Group>, + Visibility<[ClangOption, CC1Option]>, + HelpText< + "Output file for the source edits produced by the source transformation. " + "The output is a YAML document compatible with " + "'clang-apply-replacements'.">, + MarshallingInfoString<FrontendOpts<"SSAFSrcEditFile">>; +def _ssaf_transformation_report_file : + Joined<["--"], "ssaf-transformation-report-file=">, + MetaVarName<"<path>">, + Group<SSAF_Group>, + Visibility<[ClangOption, CC1Option]>, + HelpText< + "Output file for the transformation report produced by the source " + "transformation. The output is a SARIF 2.1.0 JSON document.">, + MarshallingInfoString<FrontendOpts<"SSAFTransformationReportFile">>; def Xarch__ : JoinedAndSeparate<["-"], "Xarch_">, Flags<[NoXarchOption]>, diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h b/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h new file mode 100644 index 0000000000000..bfde3646a7dcf --- /dev/null +++ b/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h @@ -0,0 +1,34 @@ +//===- SourceTransformationFrontendAction.h ---------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H +#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H + +#include "clang/Frontend/FrontendAction.h" +#include <memory> + +namespace clang::ssaf { + +/// Wraps the existing \c FrontendAction and runs the source-transformation +/// pipeline alongside it. The transformation consumes a \c WPASuite read +/// from \c FrontendOptions::SSAFGlobalScopeAnalysisResult and emits source +/// edits and a transformation report to the configured output files. +class SourceTransformationFrontendAction final : public WrapperFrontendAction { +public: + explicit SourceTransformationFrontendAction( + std::unique_ptr<FrontendAction> WrappedAction); + ~SourceTransformationFrontendAction(); + +protected: + std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, + StringRef InFile) override; +}; + +} // namespace clang::ssaf + +#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index 312e8f8c69f0a..d1d8ea658d937 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -7912,6 +7912,10 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA, Args.AddLastArg(CmdArgs, options::OPT__ssaf_extract_summaries); Args.AddLastArg(CmdArgs, options::OPT__ssaf_tu_summary_file); Args.AddLastArg(CmdArgs, options::OPT__ssaf_compilation_unit_id); + Args.AddLastArg(CmdArgs, options::OPT__ssaf_source_transformation); + Args.AddLastArg(CmdArgs, options::OPT__ssaf_global_scope_analysis_result); + Args.AddLastArg(CmdArgs, options::OPT__ssaf_src_edit_file); + Args.AddLastArg(CmdArgs, options::OPT__ssaf_transformation_report_file); // Handle serialized diagnostics. if (Arg *A = Args.getLastArg(options::OPT__serialize_diags)) { diff --git a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp index e4622496758ac..7bf272acd75ac 100644 --- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp +++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp @@ -23,6 +23,7 @@ #include "clang/FrontendTool/Utils.h" #include "clang/Options/Options.h" #include "clang/Rewrite/Frontend/FrontendActions.h" +#include "clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h" #include "clang/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendAction.h" #include "clang/ScalableStaticAnalysisFramework/SSAFForceLinker.h" // IWYU pragma: keep #include "clang/StaticAnalyzer/Frontend/AnalyzerHelpFlags.h" @@ -213,6 +214,12 @@ CreateFrontendAction(CompilerInstance &CI) { Act = std::make_unique<ssaf::TUSummaryExtractorFrontendAction>( std::move(Act)); } + if (!FEOpts.SSAFSourceTransformation.empty() || + !FEOpts.SSAFSrcEditFile.empty() || + !FEOpts.SSAFTransformationReportFile.empty()) { + Act = std::make_unique<ssaf::SourceTransformationFrontendAction>( + std::move(Act)); + } return Act; } diff --git a/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt index 3da1558810572..72d56c1a4b42e 100644 --- a/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt +++ b/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt @@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS ) add_clang_library(clangScalableStaticAnalysisFrameworkFrontend + SourceTransformationFrontendAction.cpp TUSummaryExtractorFrontendAction.cpp LINK_LIBS @@ -11,5 +12,7 @@ add_clang_library(clangScalableStaticAnalysisFrameworkFrontend clangFrontend clangScalableStaticAnalysisFrameworkAnalyses clangScalableStaticAnalysisFrameworkCore + clangScalableStaticAnalysisFrameworkSourceTransformation clangSema + clangToolingCore ) diff --git a/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp b/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp new file mode 100644 index 0000000000000..29ac0a46b74e6 --- /dev/null +++ b/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp @@ -0,0 +1,232 @@ +//===- SourceTransformationFrontendAction.cpp -----------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h" +#include "clang/AST/ASTConsumer.h" +#include "clang/Basic/DiagnosticFrontend.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/MultiplexConsumer.h" +#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h" +#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormatRegistry.h" +#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h" +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/IOSandbox.h" +#include "llvm/Support/Path.h" +#include <memory> +#include <string> +#include <utility> +#include <vector> + +using namespace clang; +using namespace ssaf; + +namespace { + +/// Concrete `SourceEditEmitter` that buffers replacements until flushed. +class AccumulatorSourceEditEmitter final : public SourceEditEmitter { +public: + void addReplacement(clang::tooling::Replacement R) override { + Replacements.push_back(std::move(R)); + } + + std::vector<clang::tooling::Replacement> Replacements; +}; + +/// Concrete `TransformationReportEmitter` that buffers results until flushed. +class AccumulatorReportEmitter final : public TransformationReportEmitter { +public: + void addResult(StringRef RuleId, clang::SarifResultLevel Level, + clang::CharSourceRange Range, StringRef Message) override { + Results.push_back({RuleId.str(), Level, Range, Message.str()}); + } + + std::vector<ReportResult> Results; +}; + +/// Per-TU runner: owns the loaded `WPASuite`, the accumulator emitters, and +/// the user-supplied `Transformation`. Inherits from `MultiplexConsumer` so +/// the transformation's `ASTConsumer` virtuals are forwarded for free; +/// serializes both outputs after the AST walk completes. +class SourceTransformationRunner final : public MultiplexConsumer { +public: + static std::unique_ptr<SourceTransformationRunner> + create(CompilerInstance &CI, StringRef InFile); + +private: + SourceTransformationRunner(WPASuite Suite, const FrontendOptions &Opts, + StringRef InFile); + + void HandleTranslationUnit(ASTContext &Ctx) override; + + WPASuite Suite; + AccumulatorSourceEditEmitter Edits; + AccumulatorReportEmitter Report; + const FrontendOptions &Opts; + std::string InFile; +}; + +} // namespace + +/// Returns the bare extension of \p Path (no leading dot), or `std::nullopt` if +/// \p Path is empty or has no recognizable extension. +static std::optional<StringRef> bareExtension(StringRef Path) { + StringRef Ext = llvm::sys::path::extension(Path); + if (!Ext.consume_front(".")) + return std::nullopt; + return Ext; +} + +/// Returns `true` if any orphan-flag warning was reported. Every missing +/// companion flag fires its own diagnostic in a single pass so the user +/// sees the full list of CLI mistakes at once. +static bool reportOrphanFlagMisuse(DiagnosticsEngine &Diags, + const FrontendOptions &Opts) { + bool Reported = false; + + if (!Opts.SSAFSourceTransformation.empty()) { + if (Opts.SSAFGlobalScopeAnalysisResult.empty()) { + Diags.Report(diag::warn_ssaf_source_transformation_requires_wpa_file); + Reported = true; + } + if (Opts.SSAFSrcEditFile.empty()) { + Diags.Report(diag::warn_ssaf_source_transformation_requires_edit_file); + Reported = true; + } + if (Opts.SSAFTransformationReportFile.empty()) { + Diags.Report(diag::warn_ssaf_source_transformation_requires_report_file); + Reported = true; + } + if (Opts.SSAFCompilationUnitId.empty()) { + Diags.Report( + diag::warn_ssaf_source_transformation_requires_compilation_unit_id); + Reported = true; + } + } else { + if (!Opts.SSAFSrcEditFile.empty()) { + Diags.Report(diag::warn_ssaf_src_edit_file_requires_transformation); + Reported = true; + } + if (!Opts.SSAFTransformationReportFile.empty()) { + Diags.Report( + diag::warn_ssaf_transformation_report_file_requires_transformation); + Reported = true; + } + } + + return Reported; +} + +std::unique_ptr<SourceTransformationRunner> +SourceTransformationRunner::create(CompilerInstance &CI, StringRef InFile) { + const FrontendOptions &Opts = CI.getFrontendOpts(); + DiagnosticsEngine &Diags = CI.getDiagnostics(); + + if (reportOrphanFlagMisuse(Diags, Opts)) + return nullptr; + if (Opts.SSAFSourceTransformation.empty()) + return nullptr; + + if (!isTransformationRegistered(Opts.SSAFSourceTransformation)) { + Diags.Report(diag::warn_ssaf_source_transformation_unknown_name) + << Opts.SSAFSourceTransformation; + return nullptr; + } + + std::optional<StringRef> WPAExt = + bareExtension(Opts.SSAFGlobalScopeAnalysisResult); + std::unique_ptr<SerializationFormat> WPAFormat = + WPAExt && isFormatRegistered(*WPAExt) ? makeFormat(*WPAExt) : nullptr; + if (!WPAFormat) { + Diags.Report(diag::warn_ssaf_read_wpa_suite_failed) + << Opts.SSAFGlobalScopeAnalysisResult << "unknown serialization format"; + return nullptr; + } + llvm::sys::sandbox::ScopedSetting Guard = llvm::sys::sandbox::scopedDisable(); + llvm::Expected<WPASuite> SuiteOrErr = + WPAFormat->readWPASuite(Opts.SSAFGlobalScopeAnalysisResult); + if (!SuiteOrErr) { + Diags.Report(diag::warn_ssaf_read_wpa_suite_failed) + << Opts.SSAFGlobalScopeAnalysisResult + << llvm::toString(SuiteOrErr.takeError()); + return nullptr; + } + + return std::unique_ptr<SourceTransformationRunner>{ + new SourceTransformationRunner(std::move(*SuiteOrErr), Opts, InFile)}; +} + +SourceTransformationRunner::SourceTransformationRunner( + WPASuite Suite, const FrontendOptions &Opts, StringRef InFile) + : MultiplexConsumer(std::vector<std::unique_ptr<ASTConsumer>>{}), + Suite(std::move(Suite)), Opts(Opts), InFile(InFile) { + // The transformation must be constructed after Suite/Edits/Report start + // their lifetimes — those references are captured in its base ctor. + std::vector<std::unique_ptr<ASTConsumer>> Consumers; + Consumers.push_back(makeTransformation(Opts.SSAFSourceTransformation, + this->Suite, Edits, Report)); + assert(Consumers.front()); + MultiplexConsumer::Consumers = std::move(Consumers); +} + +void SourceTransformationRunner::HandleTranslationUnit(ASTContext &Ctx) { + // First, run the transformation. + MultiplexConsumer::HandleTranslationUnit(Ctx); + + llvm::sys::sandbox::ScopedSetting Guard = llvm::sys::sandbox::scopedDisable(); + + // Then serialize the source edits. + clang::tooling::TranslationUnitReplacements EditDoc; + EditDoc.MainSourceFile = InFile; + EditDoc.Replacements = std::move(Edits.Replacements); + if (auto Err = writeYAMLSourceEdits(EditDoc, Opts.SSAFSrcEditFile)) { + Ctx.getDiagnostics().Report(diag::warn_ssaf_write_src_edit_failed) + << Opts.SSAFSrcEditFile << llvm::toString(std::move(Err)); + } + + // And the transformation report. + ReportDocument ReportDoc{Opts.SSAFSourceTransformation, + Ctx.getSourceManager(), std::move(Report.Results)}; + if (auto Err = writeSARIFTransformationReport( + ReportDoc, Opts.SSAFTransformationReportFile)) { + Ctx.getDiagnostics().Report( + diag::warn_ssaf_write_transformation_report_failed) + << Opts.SSAFTransformationReportFile << llvm::toString(std::move(Err)); + } +} + +SourceTransformationFrontendAction::~SourceTransformationFrontendAction() = + default; + +SourceTransformationFrontendAction::SourceTransformationFrontendAction( + std::unique_ptr<FrontendAction> WrappedAction) + : WrapperFrontendAction(std::move(WrappedAction)) {} + +std::unique_ptr<ASTConsumer> +SourceTransformationFrontendAction::CreateASTConsumer(CompilerInstance &CI, + StringRef InFile) { + auto WrappedConsumer = WrapperFrontendAction::CreateASTConsumer(CI, InFile); + if (!WrappedConsumer) + return nullptr; + + if (auto Runner = SourceTransformationRunner::create(CI, InFile)) { + CI.getCodeGenOpts().ClearASTBeforeBackend = false; + std::vector<std::unique_ptr<ASTConsumer>> Consumers; + Consumers.reserve(2); + Consumers.push_back(std::move(WrappedConsumer)); + Consumers.push_back(std::move(Runner)); + return std::make_unique<MultiplexConsumer>(std::move(Consumers)); + } + return WrappedConsumer; +} diff --git a/clang/test/Analysis/Scalable/help.cpp b/clang/test/Analysis/Scalable/help.cpp index 15d6d109360d8..0c82e21323fdb 100644 --- a/clang/test/Analysis/Scalable/help.cpp +++ b/clang/test/Analysis/Scalable/help.cpp @@ -7,8 +7,16 @@ // HELP-NEXT: Stable identifier used as the CompilationUnit namespace name of every produced SSAF TU summary. Required when '--ssaf-tu-summary-file=' is set. // HELP-NEXT: --ssaf-extract-summaries=<summary-names> // HELP-NEXT: Comma-separated list of summary names to extract +// HELP-NEXT: --ssaf-global-scope-analysis-result=<path>.<format> +// HELP-NEXT: Path to the WPASuite file containing the whole-program analysis result consumed by the source transformation. The extension selects which file format to use. // HELP-NEXT: --ssaf-list-extractors Display the list of available SSAF summary extractors // HELP-NEXT: --ssaf-list-formats Display the list of available SSAF serialization formats +// HELP-NEXT: --ssaf-source-transformation=<name> +// HELP-NEXT: Name of the SSAF source transformation to run. Exactly one transformation per invocation. +// HELP-NEXT: --ssaf-src-edit-file=<path> +// HELP-NEXT: Output file for the source edits produced by the source transformation. The output is a YAML document compatible with 'clang-apply-replacements'. +// HELP-NEXT: --ssaf-transformation-report-file=<path> +// HELP-NEXT: Output file for the transformation report produced by the source transformation. The output is a SARIF 2.1.0 JSON document. // HELP-NEXT: --ssaf-tu-summary-file=<path>.<format> // HELP-NEXT: The output file for the extracted summaries. The extension selects which file format to use. diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json new file mode 100644 index 0000000000000..80637e10fc66f --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json @@ -0,0 +1,4 @@ +{ + "id_table": [], + "results": [] +} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json new file mode 100644 index 0000000000000..fc46d6aab7f36 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json @@ -0,0 +1,25 @@ +{ + "id_table": [ + { + "id": 0, + "name": { + "namespace": [ + { "kind": "LinkUnit", "name": "test.exe" } + ], + "suffix": "", + "usr": "c:@F@foo#" + } + }, + { + "id": 1, + "name": { + "namespace": [ + { "kind": "LinkUnit", "name": "test.exe" } + ], + "suffix": "", + "usr": "c:@F@bar#" + } + } + ], + "results": [] +} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt new file mode 100644 index 0000000000000..a91194e9ca8f9 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +if(CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32) + add_subdirectory(TestTransformationPlugin) +endif() diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt new file mode 100644 index 0000000000000..8d1ca5e0a7a38 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt @@ -0,0 +1,16 @@ +# Do NOT set LLVM_LINK_COMPONENTS or clang_target_link_libraries here. +# +# Static link would produce a second copy of LLVM and Clang libraries in the +# plugin's address space alongside the copies already loaded by clang itself, +# resulting in two distinct llvm::Registry instances and broken registration. +# Let the dynamic loader resolve every symbol from clang at load time. + +add_llvm_library(SSAFTestTransformationPlugin MODULE BUILDTREE_ONLY + TestTransformation.cpp + PLUGIN_TOOL clang + ) + +target_include_directories(SSAFTestTransformationPlugin PRIVATE + $<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkCore,INTERFACE_INCLUDE_DIRECTORIES> + $<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkSourceTransformation,INTERFACE_INCLUDE_DIRECTORIES> + ) diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp new file mode 100644 index 0000000000000..c3579d181f732 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp @@ -0,0 +1,101 @@ +//===- TestTransformation.cpp ---------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// A transformation used only by lit tests for the source-edit-generation +// pipeline. It walks every function in the main source file, emits a +// zero-length `/*T*/` comment at the function body's start, and adds one +// `test-touches-function` note per visited function. Its level is always +// `Note` — the goal is to exercise the framework's plumbing, not to +// produce meaningful findings. The level rises to `Warning` when the +// input WPASuite's id table is non-empty, giving lit tests a knob to +// confirm the suite is read at all without depending on namespace +// matching. +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "clang/AST/RecursiveASTVisitor.h" +#include "clang/Basic/Sarif.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Lex/Lexer.h" +#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityIdTable.h" +#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h" +#include "clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h" +#include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/SmallString.h" + +using namespace clang; +using namespace clang::ssaf; + +namespace { + +class TestTransformation final : public Transformation { +public: + using Transformation::Transformation; + + void HandleTranslationUnit(ASTContext &Ctx) override { + bool SuiteIsNonEmpty = Suite.getIdTable().count() > 0; + Visitor V{*this, Ctx, SuiteIsNonEmpty}; + V.TraverseDecl(Ctx.getTranslationUnitDecl()); + } + +private: + class Visitor : public RecursiveASTVisitor<Visitor> { + public: + Visitor(TestTransformation &T, ASTContext &Ctx, bool SuiteIsNonEmpty) + : T(T), Ctx(Ctx), Level(SuiteIsNonEmpty + ? clang::SarifResultLevel::Warning + : clang::SarifResultLevel::Note) {} + + bool VisitFunctionDecl(FunctionDecl *FD) { + if (!FD->hasBody()) + return true; + SourceManager &SM = Ctx.getSourceManager(); + if (!SM.isInMainFile(FD->getLocation())) + return true; + + Stmt *Body = FD->getBody(); + SourceLocation BodyStart = Body->getBeginLoc(); + if (BodyStart.isInvalid()) + return true; + + llvm::SmallString<64> FilePath(SM.getFilename(BodyStart)); + unsigned Offset = SM.getFileOffset(BodyStart); + T.Edits.addReplacement( + clang::tooling::Replacement(FilePath, Offset, /*Length=*/0, "/*T*/")); + + CharSourceRange Range = Lexer::getAsCharRange( + CharSourceRange::getTokenRange(FD->getNameInfo().getSourceRange()), + SM, Ctx.getLangOpts()); + std::string Message = "visited " + FD->getNameAsString(); + T.Report.addResult("test-touches-function", Level, Range, Message); + return true; + } + + private: + TestTransformation &T; + ASTContext &Ctx; + clang::SarifResultLevel Level; + }; +}; + +} // namespace + +namespace clang::ssaf { +// NOLINTNEXTLINE(misc-use-internal-linkage) +volatile int SSAFTestTransformationAnchorSource = 0; +} // namespace clang::ssaf + +static TransformationRegistry::Add<TestTransformation> + RegisterTestTransformation("test-transformation", + "Test transformation for the SSAF " + "source-edit-generation lit suite"); + diff --git a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg new file mode 100644 index 0000000000000..bc7dd40525f86 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg @@ -0,0 +1,2 @@ +config.suffixes = [] +config.unsupported = True diff --git a/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp b/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp new file mode 100644 index 0000000000000..a0adf61c8c6fb --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp @@ -0,0 +1,51 @@ +// CLI errors for the source-edit-generation pipeline. Every misuse of the +// four `--ssaf-{source-transformation,global-scope-analysis-result, +// src-edit-file,transformation-report-file}=` flags emits a default-error +// diagnostic under `-Wscalable-static-analysis-framework`. The runner +// produces no edit/report files and the rest of the compile pipeline is +// untouched. + +// DEFINE: %{filecheck} = FileCheck %s --match-full-lines --check-prefix +// DEFINE: %{base} = --ssaf-source-transformation=does-not-exist \ +// DEFINE: --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \ +// DEFINE: --ssaf-src-edit-file=%t/edits.yaml \ +// DEFINE: --ssaf-transformation-report-file=%t/report.sarif \ +// DEFINE: --ssaf-compilation-unit-id=cu + +// ============================================================================= +// 1. Unknown transformation name. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: not %clang -c %s -o %t/test.o %{base} 2>&1 | %{filecheck}=UNKNOWN-NAME +// RUN: not %clang_cc1 %s %{base} 2>&1 | %{filecheck}=UNKNOWN-NAME +// UNKNOWN-NAME: error: no source transformation registered with name: does-not-exist [-Wscalable-static-analysis-framework] +// RUN: not test -e %t/edits.yaml +// RUN: not test -e %t/report.sarif + +// ============================================================================= +// 2. Orphan companion flags: --ssaf-source-transformation= alone. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: not %clang -c %s -o %t/test.o --ssaf-source-transformation=does-not-exist 2>&1 | %{filecheck}=ORPHAN-COMPANIONS +// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' requires '--ssaf-global-scope-analysis-result=' to be set [-Wscalable-static-analysis-framework] +// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' requires '--ssaf-src-edit-file=' to be set [-Wscalable-static-analysis-framework] +// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' requires '--ssaf-transformation-report-file=' to be set [-Wscalable-static-analysis-framework] +// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' requires '--ssaf-compilation-unit-id=' to be set [-Wscalable-static-analysis-framework] + +// ============================================================================= +// 3. Reverse orphans: edit/report file set without transformation flag. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: not %clang -c %s -o %t/test.o --ssaf-src-edit-file=%t/e.yaml 2>&1 | %{filecheck}=ORPHAN-EDIT +// ORPHAN-EDIT: error: option '--ssaf-src-edit-file=' requires '--ssaf-source-transformation=' to be set [-Wscalable-static-analysis-framework] +// RUN: not test -e %t/e.yaml + +// RUN: rm -rf %t && mkdir -p %t +// RUN: not %clang -c %s -o %t/test.o --ssaf-transformation-report-file=%t/r.sarif 2>&1 | %{filecheck}=ORPHAN-REPORT +// ORPHAN-REPORT: error: option '--ssaf-transformation-report-file=' requires '--ssaf-source-transformation=' to be set [-Wscalable-static-analysis-framework] +// RUN: not test -e %t/r.sarif + +void foo() {} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp b/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp new file mode 100644 index 0000000000000..85268c1d0dc62 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp @@ -0,0 +1,33 @@ +// Stage-1 (TU-summary extraction) and stage-2 (source-edit generation) can +// both be active in a single clang invocation. Their flags do not interact +// at the data layer — the source transformation reads its WPASuite from +// disk, not from the in-flight extractor. The two pipelines stack as +// independent ASTConsumers; both produce their per-TU output files. + +// REQUIRES: plugins + +// RUN: rm -rf %t && mkdir -p %t +// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \ +// RUN: --ssaf-extract-summaries=CallGraph \ +// RUN: --ssaf-tu-summary-file=%t/tu.json \ +// RUN: --ssaf-source-transformation=test-transformation \ +// RUN: --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \ +// RUN: --ssaf-src-edit-file=%t/edits.yaml \ +// RUN: --ssaf-transformation-report-file=%t/report.sarif \ +// RUN: --ssaf-compilation-unit-id=cu \ +// RUN: -emit-obj -o %t/test.o %s + +// All four artifacts must be present. +// RUN: test -e %t/test.o +// RUN: test -e %t/tu.json +// RUN: test -e %t/edits.yaml +// RUN: test -e %t/report.sarif + +// And the source-transformation outputs are non-trivial (the plugin emits +// one replacement and one finding per function in the main file). +// RUN: FileCheck --check-prefix=EDITS --input-file=%t/edits.yaml %s +// EDITS: ReplacementText: '/*T*/' +// RUN: FileCheck --check-prefix=REPORT --input-file=%t/report.sarif %s +// REPORT: "ruleId": "test-touches-function" + +void foo() {} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp b/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp new file mode 100644 index 0000000000000..23855a67f4572 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp @@ -0,0 +1,35 @@ +// The source-edit-generation diagnostics are downgradable via +// `-Wno-error=scalable-static-analysis-framework` and silenceable via +// `-Wno-scalable-static-analysis-framework`. In both cases the +// compilation continues normally and produces its object file, but no +// edit or report file is written. + +// DEFINE: %{filecheck} = FileCheck %s --match-full-lines --check-prefix +// DEFINE: %{flags} = --ssaf-source-transformation=does-not-exist \ +// DEFINE: --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \ +// DEFINE: --ssaf-src-edit-file=%t/edits.yaml \ +// DEFINE: --ssaf-transformation-report-file=%t/report.sarif \ +// DEFINE: --ssaf-compilation-unit-id=cu + +// ============================================================================= +// 1. -Wno-error=scalable-static-analysis-framework downgrades to a warning. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: %clang -c %s -o %t/test.o -Wno-error=scalable-static-analysis-framework %{flags} 2>&1 | %{filecheck}=WARNING +// WARNING: warning: no source transformation registered with name: does-not-exist [-Wscalable-static-analysis-framework] +// RUN: test -e %t/test.o +// RUN: not test -e %t/edits.yaml +// RUN: not test -e %t/report.sarif + +// ============================================================================= +// 2. -Wno-scalable-static-analysis-framework silences the diagnostic. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: %clang -c %s -o %t/test.o -Wno-scalable-static-analysis-framework %{flags} 2>&1 | count 0 +// RUN: test -e %t/test.o +// RUN: not test -e %t/edits.yaml +// RUN: not test -e %t/report.sarif + +void foo() {} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp b/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp new file mode 100644 index 0000000000000..d22c901217c31 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp @@ -0,0 +1,37 @@ +// End-to-end test of the source-edit-generation pipeline driven by the +// `test-transformation` plugin. Walks every function in the main source +// file, inserts a zero-length `/*T*/` comment at each function body's +// start, and emits one `test-touches-function` finding per function. +// The finding's level is `Warning` if the function's USR is in the input +// WPASuite, `Note` otherwise — verifying the WPASuite is actually read. + +// REQUIRES: plugins + +// RUN: rm -rf %t && mkdir -p %t +// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \ +// RUN: --ssaf-source-transformation=test-transformation \ +// RUN: --ssaf-global-scope-analysis-result=%S/Inputs/two-function-suite.json \ +// RUN: --ssaf-src-edit-file=%t/edits.yaml \ +// RUN: --ssaf-transformation-report-file=%t/report.sarif \ +// RUN: --ssaf-compilation-unit-id=cu \ +// RUN: -emit-obj -o %t/test.o %s + +// RUN: FileCheck --check-prefix=EDITS --input-file=%t/edits.yaml %s +// EDITS: MainSourceFile: {{.*}}happy-path.cpp +// EDITS: Replacements: +// EDITS-DAG: Offset: {{[0-9]+}} +// EDITS-DAG: ReplacementText: '/*T*/' + +// RUN: FileCheck --check-prefix=REPORT --input-file=%t/report.sarif %s +// REPORT-DAG: "name": "clang-ssaf" +// REPORT-DAG: "fullName": {{.*}}test-transformation +// REPORT-DAG: "ruleId": "test-touches-function" +// REPORT-DAG: "level": "warning" +// REPORT-DAG: "uri": "file://{{.*}}happy-path.cpp" + +// `foo` and `bar` are in two-function-suite.json's id_table, so their +// findings escalate from Note to Warning. `baz` is not — its finding +// stays at Note. +void foo() {} +void bar() {} +void baz() {} diff --git a/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp b/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp new file mode 100644 index 0000000000000..74ef0421045a8 --- /dev/null +++ b/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp @@ -0,0 +1,41 @@ +// When the source-edit or transformation-report writer's `write` returns an +// `llvm::Error`, the framework reports a default-error diagnostic. With +// `-Wno-error=scalable-static-analysis-framework` the diagnostic downgrades +// to a warning and the rest of the compile pipeline finishes normally. + +// REQUIRES: plugins + +// RUN: rm -rf %t && mkdir -p %t + +// ============================================================================= +// 1. Source-edit write fails because the parent directory does not exist. +// ============================================================================= + +// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \ +// RUN: -Wno-error=scalable-static-analysis-framework \ +// RUN: --ssaf-source-transformation=test-transformation \ +// RUN: --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \ +// RUN: --ssaf-src-edit-file=%t/missing-dir/edits.yaml \ +// RUN: --ssaf-transformation-report-file=%t/report.sarif \ +// RUN: --ssaf-compilation-unit-id=cu \ +// RUN: -emit-obj -o %t/test.o %s 2>&1 | FileCheck --check-prefix=EDIT-FAIL %s +// EDIT-FAIL: warning: failed to write source edits to '{{.*}}/missing-dir/edits.yaml'{{.*}}[-Wscalable-static-analysis-framework] +// RUN: test -e %t/test.o + +// ============================================================================= +// 2. Transformation-report write fails. +// ============================================================================= + +// RUN: rm -rf %t && mkdir -p %t +// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \ +// RUN: -Wno-error=scalable-static-analysis-framework \ +// RUN: --ssaf-source-transformation=test-transformation \ +// RUN: --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \ +// RUN: --ssaf-src-edit-file=%t/edits.yaml \ +// RUN: --ssaf-transformation-report-file=%t/missing-dir/report.sarif \ +// RUN: --ssaf-compilation-unit-id=cu \ +// RUN: -emit-obj -o %t/test.o %s 2>&1 | FileCheck --check-prefix=REPORT-FAIL %s +// REPORT-FAIL: warning: failed to write transformation report to '{{.*}}/missing-dir/report.sarif'{{.*}}[-Wscalable-static-analysis-framework] +// RUN: test -e %t/test.o + +void foo() {} diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt index 8dd0084c53224..6cb6877fed55c 100644 --- a/clang/test/CMakeLists.txt +++ b/clang/test/CMakeLists.txt @@ -211,7 +211,9 @@ endif() if(CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32) list(APPEND CLANG_TEST_DEPS SSAFExamplePlugin + SSAFTestTransformationPlugin ) + add_subdirectory(Analysis/Scalable/source-edit-generation/Plugins) endif() if (HAVE_CLANG_REPL_SUPPORT) _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
