[clang] [clang][ssaf] Add `clang-ssaf-analyzer` (PR #196124)
Aviral Goel via cfe-commits
cfe-commits at lists.llvm.org
Wed May 6 10:39:12 PDT 2026
https://github.com/aviralg updated https://github.com/llvm/llvm-project/pull/196124
>From 7cce97d9c4bb79636116e81d4a74416996b29974 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 6 May 2026 10:14:25 -0700
Subject: [PATCH] [clang][ssaf] Add `clang-ssaf-analyzer`
---
.../Tool/Utils.cpp | 4 +
.../ssaf-analyzer/Inputs/lu-badext.txt | 1 +
.../ssaf-analyzer/Inputs/lu-corrupt.json | 1 +
.../Scalable/ssaf-analyzer/Inputs/lu-noext | 1 +
.../ssaf-analyzer/Inputs/lu-tags-only.json | 44 ++++++
.../ssaf-analyzer/Inputs/lu-wrong-type.json | 1 +
.../Scalable/ssaf-analyzer/Inputs/lu.json | 126 ++++++++++++++++
.../Scalable/ssaf-analyzer/Outputs/all.json | 90 +++++++++++
.../Scalable/ssaf-analyzer/Outputs/both.json | 81 ++++++++++
.../Scalable/ssaf-analyzer/Outputs/pairs.json | 70 +++++++++
.../Scalable/ssaf-analyzer/Outputs/tags.json | 56 +++++++
.../Scalable/ssaf-analyzer/analyzer.test | 141 ++++++++++++++++++
.../Scalable/ssaf-analyzer/cli-errors.test | 53 +++++++
.../Analysis/Scalable/ssaf-analyzer/help.test | 23 +++
.../Scalable/ssaf-analyzer/lit.local.cfg | 4 +
.../validation-errors-permissions.test | 28 ++++
.../ssaf-analyzer/validation-errors.test | 45 ++++++
.../Scalable/ssaf-analyzer/version.test | 5 +
clang/test/CMakeLists.txt | 1 +
clang/tools/CMakeLists.txt | 1 +
.../tools/clang-ssaf-analyzer/CMakeLists.txt | 25 ++++
.../clang-ssaf-analyzer/SSAFAnalyzer.cpp | 134 +++++++++++++++++
22 files changed, 935 insertions(+)
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-badext.txt
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-corrupt.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-noext
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-wrong-type.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/all.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/both.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/pairs.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/tags.json
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/cli-errors.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/help.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/lit.local.cfg
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors-permissions.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/version.test
create mode 100644 clang/tools/clang-ssaf-analyzer/CMakeLists.txt
create mode 100644 clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Tool/Utils.cpp b/clang/lib/ScalableStaticAnalysisFramework/Tool/Utils.cpp
index 6a78620b757e6..f14f34eb3bbb0 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Tool/Utils.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Tool/Utils.cpp
@@ -136,6 +136,10 @@ llvm::StringRef clang::ssaf::getToolName() { return ToolName; }
void clang::ssaf::loadPlugins(llvm::ArrayRef<std::string> Paths) {
for (const std::string &PluginPath : Paths) {
+ if (!fs::exists(PluginPath)) {
+ fail(ErrorMessages::FailedToLoadPlugin, PluginPath,
+ ErrorMessages::PathDoesNotExist);
+ }
std::string ErrMsg;
if (llvm::sys::DynamicLibrary::LoadLibraryPermanently(PluginPath.c_str(),
&ErrMsg)) {
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-badext.txt b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-badext.txt
new file mode 100644
index 0000000000000..0967ef424bce6
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-badext.txt
@@ -0,0 +1 @@
+{}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-corrupt.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-corrupt.json
new file mode 100644
index 0000000000000..b22e81a41ba54
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-corrupt.json
@@ -0,0 +1 @@
+{ this is not valid json }
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-noext b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-noext
new file mode 100644
index 0000000000000..0967ef424bce6
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-noext
@@ -0,0 +1 @@
+{}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
new file mode 100644
index 0000000000000..c562a25f3c55d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
@@ -0,0 +1,44 @@
+{
+ "data": [
+ {
+ "summary_name": "TagsEntitySummary",
+ "summary_data": [
+ {
+ "entity_id": 0,
+ "entity_summary": {
+ "tags": ["important", "public"]
+ }
+ }
+ ]
+ }
+ ],
+ "id_table": [
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ }
+ ],
+ "linkage_table": [
+ {
+ "id": 0,
+ "linkage": {
+ "type": "External"
+ }
+ }
+ ],
+ "lu_namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-wrong-type.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-wrong-type.json
new file mode 100644
index 0000000000000..b5d8bb58d9bc3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-wrong-type.json
@@ -0,0 +1 @@
+[1, 2, 3]
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
new file mode 100644
index 0000000000000..9c30fefc55dd5
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
@@ -0,0 +1,126 @@
+{
+ "data": [
+ {
+ "summary_name": "TagsEntitySummary",
+ "summary_data": [
+ {
+ "entity_id": 0,
+ "entity_summary": {
+ "tags": ["important", "public"]
+ }
+ },
+ {
+ "entity_id": 1,
+ "entity_summary": {
+ "tags": ["internal", "deprecated", "important"]
+ }
+ },
+ {
+ "entity_id": 2,
+ "entity_summary": {
+ "tags": ["public"]
+ }
+ }
+ ]
+ },
+ {
+ "summary_name": "PairsEntitySummary",
+ "summary_data": [
+ {
+ "entity_id": 0,
+ "entity_summary": {
+ "pairs": [
+ {"first": {"@": 1}, "second": {"@": 2}}
+ ]
+ }
+ },
+ {
+ "entity_id": 1,
+ "entity_summary": {
+ "pairs": [
+ {"first": {"@": 0}, "second": {"@": 2}},
+ {"first": {"@": 2}, "second": {"@": 0}}
+ ]
+ }
+ },
+ {
+ "entity_id": 2,
+ "entity_summary": {
+ "pairs": [
+ {"first": {"@": 0}, "second": {"@": 1}},
+ {"first": {"@": 1}, "second": {"@": 0}},
+ {"first": {"@": 0}, "second": {"@": 0}}
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "id_table": [
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ },
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at bar#"
+ }
+ },
+ {
+ "id": 2,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at baz#"
+ }
+ }
+ ],
+ "linkage_table": [
+ {
+ "id": 0,
+ "linkage": {
+ "type": "External"
+ }
+ },
+ {
+ "id": 1,
+ "linkage": {
+ "type": "External"
+ }
+ },
+ {
+ "id": 2,
+ "linkage": {
+ "type": "External"
+ }
+ }
+ ],
+ "lu_namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/all.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/all.json
new file mode 100644
index 0000000000000..d408b64d05af3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/all.json
@@ -0,0 +1,90 @@
+{
+ "id_table": [
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at bar#"
+ }
+ },
+ {
+ "id": 2,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at baz#"
+ }
+ },
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ }
+ ],
+ "results": [
+ {
+ "analysis_name": "PairsAnalysisResult",
+ "result": {
+ "pair_counts": [
+ {
+ "count": 1,
+ "entity_id": {
+ "@": 0
+ }
+ },
+ {
+ "count": 2,
+ "entity_id": {
+ "@": 1
+ }
+ },
+ {
+ "count": 3,
+ "entity_id": {
+ "@": 2
+ }
+ }
+ ]
+ }
+ },
+ {
+ "analysis_name": "TagsAnalysisResult",
+ "result": {
+ "tags": [
+ "deprecated",
+ "important",
+ "internal",
+ "public"
+ ]
+ }
+ },
+ {
+ "analysis_name": "TagsPairsAnalysisResult",
+ "result": {
+ "entity_count": 3,
+ "max_pairs_per_entity": 3,
+ "total_pair_count": 6,
+ "unique_tag_count": 4
+ }
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/both.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/both.json
new file mode 100644
index 0000000000000..e5f7170814295
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/both.json
@@ -0,0 +1,81 @@
+{
+ "id_table": [
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at bar#"
+ }
+ },
+ {
+ "id": 2,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at baz#"
+ }
+ },
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ }
+ ],
+ "results": [
+ {
+ "analysis_name": "PairsAnalysisResult",
+ "result": {
+ "pair_counts": [
+ {
+ "count": 1,
+ "entity_id": {
+ "@": 0
+ }
+ },
+ {
+ "count": 2,
+ "entity_id": {
+ "@": 1
+ }
+ },
+ {
+ "count": 3,
+ "entity_id": {
+ "@": 2
+ }
+ }
+ ]
+ }
+ },
+ {
+ "analysis_name": "TagsAnalysisResult",
+ "result": {
+ "tags": [
+ "deprecated",
+ "important",
+ "internal",
+ "public"
+ ]
+ }
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/pairs.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/pairs.json
new file mode 100644
index 0000000000000..3f52b0acfd900
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/pairs.json
@@ -0,0 +1,70 @@
+{
+ "id_table": [
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at bar#"
+ }
+ },
+ {
+ "id": 2,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at baz#"
+ }
+ },
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ }
+ ],
+ "results": [
+ {
+ "analysis_name": "PairsAnalysisResult",
+ "result": {
+ "pair_counts": [
+ {
+ "count": 1,
+ "entity_id": {
+ "@": 0
+ }
+ },
+ {
+ "count": 2,
+ "entity_id": {
+ "@": 1
+ }
+ },
+ {
+ "count": 3,
+ "entity_id": {
+ "@": 2
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/tags.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/tags.json
new file mode 100644
index 0000000000000..aa41194746ea9
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Outputs/tags.json
@@ -0,0 +1,56 @@
+{
+ "id_table": [
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at bar#"
+ }
+ },
+ {
+ "id": 2,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at baz#"
+ }
+ },
+ {
+ "id": 0,
+ "name": {
+ "namespace": [
+ {
+ "kind": "LinkUnit",
+ "name": "test.exe"
+ }
+ ],
+ "suffix": "",
+ "usr": "c:@F at foo#"
+ }
+ }
+ ],
+ "results": [
+ {
+ "analysis_name": "TagsAnalysisResult",
+ "result": {
+ "tags": [
+ "deprecated",
+ "important",
+ "internal",
+ "public"
+ ]
+ }
+ }
+ ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test b/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test
new file mode 100644
index 0000000000000..0abdcef15a449
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/analyzer.test
@@ -0,0 +1,141 @@
+// Tests for clang-ssaf-analyzer that require plugin support.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Error: unknown analysis name
+// ============================================================================
+
+// RUN: not %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/unknown.json -a NoSuchAnalysis 2>&1 \
+// RUN: | FileCheck %s --check-prefix=UNKNOWN
+// UNKNOWN: no analysis registered for 'AnalysisName(NoSuchAnalysis)'
+
+// ============================================================================
+// Error: valid analysis name but LUSummary lacks entity data for it
+// ============================================================================
+
+// RUN: not %clang-ssaf-analyzer-with-plugin %S/Inputs/lu-tags-only.json \
+// RUN: -o %t/missing-data.json -a PairsAnalysisResult 2>&1 \
+// RUN: | FileCheck %s --check-prefix=MISSING-DATA
+// MISSING-DATA: no data for analysis 'AnalysisName(PairsAnalysisResult)' in LUSummary
+
+// ============================================================================
+// Success: run TagsAnalysisResult only (single analysis)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/tags.json -a TagsAnalysisResult
+// RUN: diff %S/Outputs/tags.json %t/tags.json
+
+// ============================================================================
+// Success: run PairsAnalysisResult only (single analysis)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/pairs.json -a PairsAnalysisResult
+// RUN: diff %S/Outputs/pairs.json %t/pairs.json
+
+// ============================================================================
+// Success: run two summary analyses
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/both.json -a TagsAnalysisResult -a PairsAnalysisResult
+// RUN: diff %S/Outputs/both.json %t/both.json
+
+// ============================================================================
+// Success: reversed order produces the same result (order-independent)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/both-rev.json -a PairsAnalysisResult -a TagsAnalysisResult
+// RUN: diff %S/Outputs/both.json %t/both-rev.json
+
+// ============================================================================
+// Success: duplicate analysis name is deduplicated
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/tags-dup.json -a TagsAnalysisResult -a TagsAnalysisResult
+// RUN: diff %S/Outputs/tags.json %t/tags-dup.json
+
+// ============================================================================
+// Success: derived analysis pulls dependencies automatically
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/all.json -a TagsPairsAnalysisResult
+// RUN: diff %S/Outputs/all.json %t/all.json
+
+// ============================================================================
+// Success: derived + one explicit dep (redundant dep is deduplicated)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/all-partial-dup.json -a TagsPairsAnalysisResult -a TagsAnalysisResult
+// RUN: diff %S/Outputs/all.json %t/all-partial-dup.json
+
+// ============================================================================
+// Success: derived + both explicit deps (all redundant, deduplicated)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/all-full-dup.json \
+// RUN: -a TagsPairsAnalysisResult -a TagsAnalysisResult -a PairsAnalysisResult
+// RUN: diff %S/Outputs/all.json %t/all-full-dup.json
+
+// ============================================================================
+// Success: run all example-plugin analyses explicitly
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/all-default.json \
+// RUN: -a TagsAnalysisResult -a PairsAnalysisResult -a TagsPairsAnalysisResult
+// RUN: diff %S/Outputs/all.json %t/all-default.json
+
+// ============================================================================
+// Success: --analysis alias for -a
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/analysis-alias.json --analysis TagsAnalysisResult
+// RUN: diff %S/Outputs/tags.json %t/analysis-alias.json
+
+// ============================================================================
+// Success: -l alias for --load
+// ============================================================================
+
+// RUN: clang-ssaf-analyzer -l %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN: %S/Inputs/lu.json -o %t/load-alias.json -a PairsAnalysisResult
+// RUN: diff %S/Outputs/pairs.json %t/load-alias.json
+
+// ============================================================================
+// Error: mix of valid and invalid analysis names (partial failure)
+// ============================================================================
+
+// RUN: not %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/mixed.json -a TagsAnalysisResult -a NoSuchAnalysis -a PairsAnalysisResult 2>&1 \
+// RUN: | FileCheck %s --check-prefix=MIXED
+// MIXED: no analysis registered for 'AnalysisName(NoSuchAnalysis)'
+
+// ============================================================================
+// Success: -o specified twice (last value wins)
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/ignored.json -o %t/last-wins.json -a TagsAnalysisResult
+// RUN: diff %S/Outputs/tags.json %t/last-wins.json
+
+// ============================================================================
+// Round-trip: analyze then read back with ssaf-format
+// ============================================================================
+
+// RUN: %clang-ssaf-analyzer-with-plugin %S/Inputs/lu.json \
+// RUN: -o %t/roundtrip.json -a TagsPairsAnalysisResult
+// RUN: clang-ssaf-format --type wpa --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN: %t/roundtrip.json -o %t/roundtrip-copy.json
+// RUN: diff %S/Outputs/all.json %t/roundtrip-copy.json
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/cli-errors.test b/clang/test/Analysis/Scalable/ssaf-analyzer/cli-errors.test
new file mode 100644
index 0000000000000..0367ba8b8c484
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/cli-errors.test
@@ -0,0 +1,53 @@
+// Tests for clang-ssaf-analyzer CLI errors that do not require plugins.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// CLI error: no arguments
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer 2>&1 \
+// RUN: | FileCheck %s --check-prefix=NO-ARGS
+// NO-ARGS: clang-ssaf-analyzer{{(\.exe)?}}: Not enough positional command line arguments specified!
+
+// ============================================================================
+// CLI error: missing -o
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -a Foo 2>&1 \
+// RUN: | FileCheck %s --check-prefix=NO-OUTPUT
+// NO-OUTPUT: clang-ssaf-analyzer{{(\.exe)?}}: for the -o option: must be specified at least once!
+
+// ============================================================================
+// Error: nonexistent plugin path
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer --load /nonexistent/path/plugin.so \
+// RUN: %S/Inputs/lu.json -o %t/plugin-err.json 2>&1 \
+// RUN: | FileCheck %s --check-prefix=BAD-PLUGIN
+// BAD-PLUGIN: clang-ssaf-analyzer: error: failed to load plugin '/nonexistent/path/plugin.so': Path does not exist
+
+// ============================================================================
+// Error: empty analysis name
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o %t/empty-name.json -a "" 2>&1 \
+// RUN: | FileCheck %s --check-prefix=EMPTY-NAME
+// EMPTY-NAME: clang-ssaf-analyzer: error: analysis name must not be empty
+
+// ============================================================================
+// Error: corrupt input JSON (parse error)
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-corrupt.json -o %t/corrupt.json 2>&1 \
+// RUN: | FileCheck %s --check-prefix=CORRUPT
+// CORRUPT: clang-ssaf-analyzer: error: reading LUSummary from file '{{.*}}lu-corrupt.json'
+
+// ============================================================================
+// Error: valid JSON but wrong top-level type (array instead of object)
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-wrong-type.json -o %t/wrong-type.json 2>&1 \
+// RUN: | FileCheck %s --check-prefix=WRONG-TYPE
+// WRONG-TYPE: clang-ssaf-analyzer: error: reading LUSummary from file '{{.*}}lu-wrong-type.json'
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/help.test b/clang/test/Analysis/Scalable/ssaf-analyzer/help.test
new file mode 100644
index 0000000000000..e4b5eca320b08
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/help.test
@@ -0,0 +1,23 @@
+// Test clang-ssaf-analyzer help option
+
+// RUN: clang-ssaf-analyzer --help-list-hidden \
+// RUN: | FileCheck %s --match-full-lines
+
+// CHECK: OVERVIEW: SSAF Analyzer
+// CHECK-EMPTY:
+// CHECK-NEXT: USAGE: clang-ssaf-analyzer{{(\.exe)?}} [options] <input file>
+// CHECK-EMPTY:
+// CHECK-NEXT: OPTIONS:
+// CHECK-NEXT: -a <name> - Analysis name to run
+// CHECK-NEXT: --analysis - Alias for -a
+// CHECK-NEXT: -h - Alias for --help
+// CHECK-NEXT: --help - Display available options (--help-hidden for more)
+// CHECK-NEXT: --help-hidden - Display all available options
+// CHECK-NEXT: --help-list - Display list of available options (--help-list-hidden for more)
+// CHECK-NEXT: --help-list-hidden - Display list of all available options
+// CHECK-NEXT: -l - Alias for --load
+// CHECK-NEXT: --load=<path> - Load a plugin shared library
+// CHECK-NEXT: -o <path> - Output file path
+// CHECK-NEXT: --print-all-options - Print all option values after command line parsing
+// CHECK-NEXT: --print-options - Print non-default options after command line parsing
+// CHECK-NEXT: --version - Display the version of this program
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/lit.local.cfg b/clang/test/Analysis/Scalable/ssaf-analyzer/lit.local.cfg
new file mode 100644
index 0000000000000..59a179abf4d3b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/lit.local.cfg
@@ -0,0 +1,4 @@
+config.substitutions.insert(0,
+ ("%clang-ssaf-analyzer-with-plugin",
+ "clang-ssaf-analyzer --load {}/SSAFExamplePlugin{}".format(
+ config.llvm_shlib_dir, config.llvm_plugin_ext)))
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors-permissions.test b/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors-permissions.test
new file mode 100644
index 0000000000000..bcd4f036ac550
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors-permissions.test
@@ -0,0 +1,28 @@
+// Tests for clang-ssaf-analyzer input validation requiring file permission support.
+// UNSUPPORTED: system-windows
+// REQUIRES: non-root-user
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// Output parent directory exists but is not writable.
+// RUN: mkdir -p %t/output-dir
+// RUN: chmod -w %t/output-dir
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o %t/output-dir/output.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=NO-WRITE-PERM
+// RUN: chmod +w %t/output-dir
+// NO-WRITE-PERM: clang-ssaf-analyzer: error: failed to validate path '{{.*}}output.json': Parent directory is not writable
+
+// Input is a broken symlink.
+// RUN: ln -sf %t/lu-nonexistent %t/lu-dangling.json
+// RUN: not clang-ssaf-analyzer %t/lu-dangling.json -o %t/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=BROKEN-SYMLINK
+// BROKEN-SYMLINK: clang-ssaf-analyzer: error: failed to validate path '{{.*}}lu-dangling.json': Path does not exist
+
+// Input file exists but is not readable.
+// RUN: cp %S/Inputs/lu.json %t/lu-unreadable.json
+// RUN: chmod -r %t/lu-unreadable.json
+// RUN: not clang-ssaf-analyzer %t/lu-unreadable.json -o %t/out2.json 2>&1 \
+// RUN: | FileCheck %s --check-prefix=NO-READ-PERM
+// RUN: chmod +r %t/lu-unreadable.json
+// NO-READ-PERM: clang-ssaf-analyzer: error: reading LUSummary from file '{{.*}}lu-unreadable.json'
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors.test
new file mode 100644
index 0000000000000..692991d157c52
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/validation-errors.test
@@ -0,0 +1,45 @@
+// Tests for clang-ssaf-analyzer input validation.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// No extension on output.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o lu 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=NO-EXT-OUTPUT
+// NO-EXT-OUTPUT: clang-ssaf-analyzer: error: failed to validate path 'lu': Extension not supplied
+
+// No extension on input.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-noext -o %t/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=NO-EXT-INPUT
+// NO-EXT-INPUT: clang-ssaf-analyzer: error: failed to validate path '{{.*}}lu-noext': Extension not supplied
+
+// Invalid extension on output.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o out.txt 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=BAD-EXT-OUTPUT
+// BAD-EXT-OUTPUT: clang-ssaf-analyzer: error: failed to validate path 'out.txt': No format registered for extension 'txt'
+
+// Invalid extension on input.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-badext.txt -o %t/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=BAD-EXT-INPUT
+// BAD-EXT-INPUT: clang-ssaf-analyzer: error: failed to validate path '{{.*}}lu-badext.txt': No format registered for extension 'txt'
+
+// Output directory does not exist.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o %S/Outputs/NonExistentDirectory/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=OUTPUT-PARENT-DIR-MISSING
+// OUTPUT-PARENT-DIR-MISSING: clang-ssaf-analyzer: error: failed to validate path '{{.*}}out.json': Parent directory does not exist
+
+// Input is a directory, not a file.
+// RUN: not clang-ssaf-analyzer %S/Inputs -o %t/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=INPUT-IS-DIR
+// INPUT-IS-DIR: clang-ssaf-analyzer: error: failed to validate path '{{.*}}Inputs': Path is not a file
+
+// Input file does not exist.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-nonexistent.json -o %t/out.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=NO-INPUT-FILE
+// NO-INPUT-FILE: clang-ssaf-analyzer: error: failed to validate path '{{.*}}lu-nonexistent.json': Path does not exist
+
+// Output file already exists.
+// RUN: touch %t/already-exists.json
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu.json -o %t/already-exists.json 2>&1 \
+// RUN: | FileCheck %s --match-full-lines --check-prefix=OUTPUT-EXISTS
+// OUTPUT-EXISTS: clang-ssaf-analyzer: error: failed to validate path '{{.*}}already-exists.json': File already exists
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/version.test b/clang/test/Analysis/Scalable/ssaf-analyzer/version.test
new file mode 100644
index 0000000000000..a859934f60606
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/version.test
@@ -0,0 +1,5 @@
+// Test clang-ssaf-analyzer version
+
+// RUN: clang-ssaf-analyzer --version | FileCheck %s
+
+// CHECK: clang-ssaf-analyzer 0.1
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index cb1f34d2e49bc..dbd1fafae7f3e 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -104,6 +104,7 @@ list(APPEND CLANG_TEST_DEPS
clang-linker-wrapper
clang-nvlink-wrapper
clang-offload-bundler
+ clang-ssaf-analyzer
clang-ssaf-format
clang-ssaf-linker
clang-sycl-linker
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index 891043ec31f77..a16089be041bf 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -15,6 +15,7 @@ add_clang_subdirectory(clang-linker-wrapper)
add_clang_subdirectory(clang-nvlink-wrapper)
add_clang_subdirectory(clang-offload-bundler)
add_clang_subdirectory(clang-scan-deps)
+add_clang_subdirectory(clang-ssaf-analyzer)
add_clang_subdirectory(clang-ssaf-format)
add_clang_subdirectory(clang-ssaf-linker)
add_clang_subdirectory(clang-sycl-linker)
diff --git a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
new file mode 100644
index 0000000000000..67732867181b6
--- /dev/null
+++ b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(LLVM_LINK_COMPONENTS
+ Option
+ Support
+ )
+
+if(CLANG_PLUGIN_SUPPORT)
+ set(support_plugins SUPPORT_PLUGINS)
+endif()
+
+add_clang_tool(clang-ssaf-analyzer
+ SSAFAnalyzer.cpp
+ ${support_plugins}
+ )
+
+clang_target_link_libraries(clang-ssaf-analyzer
+ PRIVATE
+ clangBasic
+ clangScalableStaticAnalysisFrameworkAnalyses
+ clangScalableStaticAnalysisFrameworkCore
+ clangScalableStaticAnalysisFrameworkTool
+ )
+
+if(CLANG_PLUGIN_SUPPORT)
+ export_executable_symbols_for_plugins(clang-ssaf-analyzer)
+endif()
diff --git a/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp b/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
new file mode 100644
index 0000000000000..14f037beb60c1
--- /dev/null
+++ b/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
@@ -0,0 +1,134 @@
+//===- SSAFAnalyzer.cpp - SSAF Analyzer Tool ------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the SSAF analyzer tool that runs whole-program
+// analyses over an LUSummary and writes the resulting WPASuite to an
+// output file.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/SSAFForceLinker.h" // IWYU pragma: keep
+#include "clang/ScalableStaticAnalysisFramework/Tool/Utils.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/InitLLVM.h"
+#include <memory>
+#include <string>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Command-Line Options
+//===----------------------------------------------------------------------===//
+
+cl::OptionCategory SsafAnalyzerCategory("clang-ssaf-analyzer options");
+
+cl::opt<std::string> InputPath(cl::Positional, cl::desc("<input file>"),
+ cl::Required, cl::cat(SsafAnalyzerCategory));
+
+cl::opt<std::string> OutputPath("o", cl::desc("Output file path"),
+ cl::value_desc("path"), cl::Required,
+ cl::cat(SsafAnalyzerCategory));
+
+cl::list<std::string> AnalysisNames("a", cl::desc("Analysis name to run"),
+ cl::value_desc("name"),
+ cl::cat(SsafAnalyzerCategory));
+
+cl::alias AnalysisNamesAlias("analysis", cl::aliasopt(AnalysisNames),
+ cl::desc("Alias for -a"));
+
+cl::list<std::string> LoadPlugins("load",
+ cl::desc("Load a plugin shared library"),
+ cl::value_desc("path"),
+ cl::cat(SsafAnalyzerCategory));
+
+cl::alias LoadPluginsAlias("l", cl::aliasopt(LoadPlugins),
+ cl::desc("Alias for --load"));
+
+//===----------------------------------------------------------------------===//
+// Input Validation
+//===----------------------------------------------------------------------===//
+
+struct AnalyzerInput {
+ FormatFile InputFile;
+ FormatFile OutputFile;
+ llvm::SmallVector<AnalysisName> Names;
+};
+
+AnalyzerInput validate() {
+ AnalyzerInput AI;
+
+ // Validate the input path.
+ AI.InputFile = FormatFile::fromInputPath(InputPath);
+
+ // Validate the output path.
+ AI.OutputFile = FormatFile::fromOutputPath(OutputPath);
+
+ // Build and validate analysis names.
+ for (const auto &Name : AnalysisNames) {
+ if (Name.empty()) {
+ fail("analysis name must not be empty");
+ }
+ AI.Names.push_back(AnalysisName(Name));
+ }
+
+ return AI;
+}
+
+//===----------------------------------------------------------------------===//
+// Analysis Pipeline
+//===----------------------------------------------------------------------===//
+
+void analyze(const AnalyzerInput &AI) {
+ // Read the LUSummary.
+ auto ExpectedLU = AI.InputFile.Format->readLUSummary(AI.InputFile.Path);
+ if (!ExpectedLU) {
+ fail(ExpectedLU.takeError());
+ }
+
+ // Run analyses. If specific names were given, run only those;
+ // otherwise run all registered analyses.
+ AnalysisDriver Driver(std::make_unique<LUSummary>(std::move(*ExpectedLU)));
+ auto ExpectedSuite =
+ AI.Names.empty() ? std::move(Driver).run() : Driver.run(AI.Names);
+ if (!ExpectedSuite) {
+ fail(ExpectedSuite.takeError());
+ }
+
+ // Write the WPASuite.
+ if (auto Err = AI.OutputFile.Format->writeWPASuite(*ExpectedSuite,
+ AI.OutputFile.Path)) {
+ fail(std::move(Err));
+ }
+}
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Driver
+//===----------------------------------------------------------------------===//
+
+int main(int argc, const char **argv) {
+ llvm::StringRef ToolHeading = "SSAF Analyzer";
+
+ InitLLVM X(argc, argv);
+ initTool(argc, argv, "0.1", SsafAnalyzerCategory, ToolHeading);
+
+ loadPlugins(LoadPlugins);
+
+ AnalyzerInput AI = validate();
+ analyze(AI);
+
+ return 0;
+}
More information about the cfe-commits
mailing list