Skip to content

Commit f69c74d

Browse files
author
Charusso
committed
[analyzer] FixItHint: Apply and test hints with the Clang-Tidy's script
Summary: This patch introduces a way to apply the fix-its by the Analyzer: `-analyzer-config apply-fixits=true`. The fix-its should be testable, therefore I have copied the well-tested `check_clang_tidy.py` script. The idea is that the Analyzer's workflow is different so it would be very difficult to use only one script for both Tidy and the Analyzer, the script would diverge a lot. Example test: `// RUN: %check-analyzer-fixit %s %t -analyzer-checker=core` When the copy-paste happened the original authors were: @alexfh, @zinovy.nis, @JonasToth, @hokein, @gribozavr, @lebedev.ri Reviewed By: NoQ, alexfh, zinovy.nis Differential Revision: https://reviews.llvm.org/D69746
1 parent cac0686 commit f69c74d

File tree

7 files changed

+211
-27
lines changed

7 files changed

+211
-27
lines changed

clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ ANALYZER_OPTION(bool, ShouldEmitFixItHintsAsRemarks, "fixits-as-remarks",
310310
"Emit fix-it hints as remarks for testing purposes",
311311
false)
312312

313+
ANALYZER_OPTION(bool, ShouldApplyFixIts, "apply-fixits",
314+
"Apply the fix-it hints to the files",
315+
false)
316+
313317
//===----------------------------------------------------------------------===//
314318
// Unsigned analyzer options.
315319
//===----------------------------------------------------------------------===//

clang/lib/StaticAnalyzer/Frontend/AnalysisConsumer.cpp

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
#include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h"
1414
#include "ModelInjector.h"
15-
#include "clang/Analysis/PathDiagnostic.h"
1615
#include "clang/AST/Decl.h"
1716
#include "clang/AST/DeclCXX.h"
1817
#include "clang/AST/DeclObjC.h"
@@ -21,10 +20,12 @@
2120
#include "clang/Analysis/CFG.h"
2221
#include "clang/Analysis/CallGraph.h"
2322
#include "clang/Analysis/CodeInjector.h"
23+
#include "clang/Analysis/PathDiagnostic.h"
2424
#include "clang/Basic/SourceManager.h"
2525
#include "clang/CrossTU/CrossTranslationUnit.h"
2626
#include "clang/Frontend/CompilerInstance.h"
2727
#include "clang/Lex/Preprocessor.h"
28+
#include "clang/Rewrite/Core/Rewriter.h"
2829
#include "clang/StaticAnalyzer/Checkers/LocalCheckers.h"
2930
#include "clang/StaticAnalyzer/Core/AnalyzerOptions.h"
3031
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
@@ -33,6 +34,8 @@
3334
#include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h"
3435
#include "clang/StaticAnalyzer/Core/PathSensitive/ExprEngine.h"
3536
#include "clang/StaticAnalyzer/Frontend/CheckerRegistration.h"
37+
#include "clang/Tooling/Core/Replacement.h"
38+
#include "clang/Tooling/Tooling.h"
3639
#include "llvm/ADT/PostOrderIterator.h"
3740
#include "llvm/ADT/Statistic.h"
3841
#include "llvm/Support/FileSystem.h"
@@ -46,6 +49,7 @@
4649

4750
using namespace clang;
4851
using namespace ento;
52+
using namespace tooling;
4953

5054
#define DEBUG_TYPE "AnalysisConsumer"
5155

@@ -84,11 +88,16 @@ void ento::createTextPathDiagnosticConsumer(
8488
namespace {
8589
class ClangDiagPathDiagConsumer : public PathDiagnosticConsumer {
8690
DiagnosticsEngine &Diag;
87-
bool IncludePath = false, ShouldEmitAsError = false, FixitsAsRemarks = false;
91+
LangOptions LO;
92+
93+
bool IncludePath = false;
94+
bool ShouldEmitAsError = false;
95+
bool FixitsAsRemarks = false;
96+
bool ApplyFixIts = false;
8897

8998
public:
90-
ClangDiagPathDiagConsumer(DiagnosticsEngine &Diag)
91-
: Diag(Diag) {}
99+
ClangDiagPathDiagConsumer(DiagnosticsEngine &Diag, LangOptions LO)
100+
: Diag(Diag), LO(LO) {}
92101
~ClangDiagPathDiagConsumer() override {}
93102
StringRef getName() const override { return "ClangDiags"; }
94103

@@ -102,6 +111,7 @@ class ClangDiagPathDiagConsumer : public PathDiagnosticConsumer {
102111
void enablePaths() { IncludePath = true; }
103112
void enableWerror() { ShouldEmitAsError = true; }
104113
void enableFixitsAsRemarks() { FixitsAsRemarks = true; }
114+
void enableApplyFixIts() { ApplyFixIts = true; }
105115

106116
void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags,
107117
FilesMade *filesMade) override {
@@ -111,29 +121,44 @@ class ClangDiagPathDiagConsumer : public PathDiagnosticConsumer {
111121
: Diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0");
112122
unsigned NoteID = Diag.getCustomDiagID(DiagnosticsEngine::Note, "%0");
113123
unsigned RemarkID = Diag.getCustomDiagID(DiagnosticsEngine::Remark, "%0");
124+
SourceManager &SM = Diag.getSourceManager();
125+
126+
Replacements Repls;
127+
auto reportPiece = [&](unsigned ID, FullSourceLoc Loc, StringRef String,
128+
ArrayRef<SourceRange> Ranges,
129+
ArrayRef<FixItHint> Fixits) {
130+
if (!FixitsAsRemarks && !ApplyFixIts) {
131+
Diag.Report(Loc, ID) << String << Ranges << Fixits;
132+
return;
133+
}
134+
135+
Diag.Report(Loc, ID) << String << Ranges;
136+
if (FixitsAsRemarks) {
137+
for (const FixItHint &Hint : Fixits) {
138+
llvm::SmallString<128> Str;
139+
llvm::raw_svector_ostream OS(Str);
140+
// FIXME: Add support for InsertFromRange and
141+
// BeforePreviousInsertion.
142+
assert(!Hint.InsertFromRange.isValid() && "Not implemented yet!");
143+
assert(!Hint.BeforePreviousInsertions && "Not implemented yet!");
144+
OS << SM.getSpellingColumnNumber(Hint.RemoveRange.getBegin()) << "-"
145+
<< SM.getSpellingColumnNumber(Hint.RemoveRange.getEnd()) << ": '"
146+
<< Hint.CodeToInsert << "'";
147+
Diag.Report(Loc, RemarkID) << OS.str();
148+
}
149+
}
150+
151+
if (ApplyFixIts) {
152+
for (const FixItHint &Hint : Fixits) {
153+
Replacement Repl(SM, Hint.RemoveRange, Hint.CodeToInsert);
114154

115-
auto reportPiece =
116-
[&](unsigned ID, SourceLocation Loc, StringRef String,
117-
ArrayRef<SourceRange> Ranges, ArrayRef<FixItHint> Fixits) {
118-
if (!FixitsAsRemarks) {
119-
Diag.Report(Loc, ID) << String << Ranges << Fixits;
120-
} else {
121-
Diag.Report(Loc, ID) << String << Ranges;
122-
for (const FixItHint &Hint : Fixits) {
123-
SourceManager &SM = Diag.getSourceManager();
124-
llvm::SmallString<128> Str;
125-
llvm::raw_svector_ostream OS(Str);
126-
// FIXME: Add support for InsertFromRange and
127-
// BeforePreviousInsertion.
128-
assert(!Hint.InsertFromRange.isValid() && "Not implemented yet!");
129-
assert(!Hint.BeforePreviousInsertions && "Not implemented yet!");
130-
OS << SM.getSpellingColumnNumber(Hint.RemoveRange.getBegin())
131-
<< "-" << SM.getSpellingColumnNumber(Hint.RemoveRange.getEnd())
132-
<< ": '" << Hint.CodeToInsert << "'";
133-
Diag.Report(Loc, RemarkID) << OS.str();
134-
}
155+
if (llvm::Error Err = Repls.add(Repl)) {
156+
llvm::errs() << "Error applying replacement " << Repl.toString()
157+
<< ": " << Err << "\n";
135158
}
136-
};
159+
}
160+
}
161+
};
137162

138163
for (std::vector<const PathDiagnostic *>::iterator I = Diags.begin(),
139164
E = Diags.end();
@@ -165,6 +190,16 @@ class ClangDiagPathDiagConsumer : public PathDiagnosticConsumer {
165190
Piece->getString(), Piece->getRanges(), Piece->getFixits());
166191
}
167192
}
193+
194+
if (!ApplyFixIts || Repls.empty())
195+
return;
196+
197+
Rewriter Rewrite(SM, LO);
198+
if (!applyAllReplacements(Repls, Rewrite)) {
199+
llvm::errs() << "An error occured during applying fix-it.\n";
200+
}
201+
202+
Rewrite.overwriteChangedFiles();
168203
}
169204
};
170205
} // end anonymous namespace
@@ -257,7 +292,7 @@ class AnalysisConsumer : public AnalysisASTConsumer,
257292
if (Opts->AnalysisDiagOpt != PD_NONE) {
258293
// Create the PathDiagnosticConsumer.
259294
ClangDiagPathDiagConsumer *clangDiags =
260-
new ClangDiagPathDiagConsumer(PP.getDiagnostics());
295+
new ClangDiagPathDiagConsumer(PP.getDiagnostics(), PP.getLangOpts());
261296
PathConsumers.push_back(clangDiags);
262297

263298
if (Opts->AnalyzerWerror)
@@ -266,6 +301,9 @@ class AnalysisConsumer : public AnalysisASTConsumer,
266301
if (Opts->ShouldEmitFixItHintsAsRemarks)
267302
clangDiags->enableFixitsAsRemarks();
268303

304+
if (Opts->ShouldApplyFixIts)
305+
clangDiags->enableApplyFixIts();
306+
269307
if (Opts->AnalysisDiagOpt == PD_TEXT) {
270308
clangDiags->enablePaths();
271309

clang/lib/StaticAnalyzer/Frontend/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ add_clang_library(clangStaticAnalyzerFrontend
2121
clangLex
2222
clangStaticAnalyzerCheckers
2323
clangStaticAnalyzerCore
24+
clangRewrite
25+
clangToolingCore
2426
)

clang/test/Analysis/analyzer-config.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// CHECK-NEXT: alpha.security.MmapWriteExec:MmapProtExec = 0x04
1212
// CHECK-NEXT: alpha.security.MmapWriteExec:MmapProtRead = 0x01
1313
// CHECK-NEXT: alpha.security.taint.TaintPropagation:Config = ""
14+
// CHECK-NEXT: apply-fixits = false
1415
// CHECK-NEXT: avoid-suppressing-null-argument-paths = false
1516
// CHECK-NEXT: c++-allocator-inlining = true
1617
// CHECK-NEXT: c++-container-inlining = false
@@ -100,4 +101,4 @@
100101
// CHECK-NEXT: unroll-loops = false
101102
// CHECK-NEXT: widen-loops = false
102103
// CHECK-NEXT: [stats]
103-
// CHECK-NEXT: num-entries = 97
104+
// CHECK-NEXT: num-entries = 98
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python
2+
#
3+
#===- check-analyzer-fixit.py - Static Analyzer test helper ---*- python -*-===#
4+
#
5+
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6+
# See https://llvm.org/LICENSE.txt for license information.
7+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8+
#
9+
#===------------------------------------------------------------------------===#
10+
#
11+
# This file copy-pasted mostly from the Clang-Tidy's 'check_clang_tidy.py'.
12+
#
13+
#===------------------------------------------------------------------------===#
14+
15+
r"""
16+
Clang Static Analyzer test helper
17+
=================================
18+
19+
This script runs the Analyzer in fix-it mode and verify fixes, warnings, notes.
20+
21+
Usage:
22+
check-analyzer-fixit.py <source-file> <temp-file> [analyzer arguments]
23+
24+
Example:
25+
// RUN: %check-analyzer-fixit %s %t -analyzer-checker=core
26+
"""
27+
28+
import argparse
29+
import os
30+
import re
31+
import subprocess
32+
import sys
33+
34+
35+
def write_file(file_name, text):
36+
with open(file_name, 'w') as f:
37+
f.write(text)
38+
39+
40+
def run_test_once(args, extra_args):
41+
input_file_name = args.input_file_name
42+
temp_file_name = args.temp_file_name
43+
clang_analyzer_extra_args = extra_args
44+
45+
file_name_with_extension = input_file_name
46+
_, extension = os.path.splitext(file_name_with_extension)
47+
if extension not in ['.c', '.hpp', '.m', '.mm']:
48+
extension = '.cpp'
49+
temp_file_name = temp_file_name + extension
50+
51+
with open(input_file_name, 'r') as input_file:
52+
input_text = input_file.read()
53+
54+
# Remove the contents of the CHECK lines to avoid CHECKs matching on
55+
# themselves. We need to keep the comments to preserve line numbers while
56+
# avoiding empty lines which could potentially trigger formatting-related
57+
# checks.
58+
cleaned_test = re.sub('// *CHECK-[A-Z0-9\-]*:[^\r\n]*', '//', input_text)
59+
write_file(temp_file_name, cleaned_test)
60+
61+
original_file_name = temp_file_name + ".orig"
62+
write_file(original_file_name, cleaned_test)
63+
64+
try:
65+
builtin_include_dir = subprocess.check_output(
66+
['clang', '-print-file-name=include'], stderr=subprocess.STDOUT)
67+
except subprocess.CalledProcessError as e:
68+
print('Cannot print Clang include directory: ' + e.output.decode())
69+
70+
builtin_include_dir = os.path.normpath(builtin_include_dir)
71+
72+
args = (['clang', '-cc1', '-internal-isystem', builtin_include_dir,
73+
'-nostdsysteminc', '-analyze', '-analyzer-constraints=range',
74+
'-analyzer-config', 'apply-fixits=true']
75+
+ clang_analyzer_extra_args + ['-verify', temp_file_name])
76+
77+
print('Running ' + str(args) + '...')
78+
79+
try:
80+
clang_analyzer_output = \
81+
subprocess.check_output(args, stderr=subprocess.STDOUT).decode()
82+
except subprocess.CalledProcessError as e:
83+
print('Clang Static Analyzer test failed:\n' + e.output.decode())
84+
raise
85+
86+
print('----------------- Clang Static Analyzer output -----------------\n' +
87+
clang_analyzer_output +
88+
'\n--------------------------------------------------------------')
89+
90+
try:
91+
diff_output = subprocess.check_output(
92+
['diff', '-u', original_file_name, temp_file_name],
93+
stderr=subprocess.STDOUT)
94+
except subprocess.CalledProcessError as e:
95+
diff_output = e.output
96+
97+
print('----------------------------- Fixes ----------------------------\n' +
98+
diff_output.decode() +
99+
'\n--------------------------------------------------------------')
100+
101+
try:
102+
subprocess.check_output(
103+
['FileCheck', '-input-file=' + temp_file_name, input_file_name,
104+
'-check-prefixes=CHECK-FIXES', '-strict-whitespace'],
105+
stderr=subprocess.STDOUT)
106+
except subprocess.CalledProcessError as e:
107+
print('FileCheck failed:\n' + e.output.decode())
108+
raise
109+
110+
111+
def main():
112+
parser = argparse.ArgumentParser()
113+
parser.add_argument('input_file_name')
114+
parser.add_argument('temp_file_name')
115+
116+
args, extra_args = parser.parse_known_args()
117+
run_test_once(args, extra_args)
118+
119+
120+
if __name__ == '__main__':
121+
main()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// RUN: %check_analyzer_fixit %s %t \
2+
// RUN: -analyzer-checker=core,optin.cplusplus.VirtualCall \
3+
// RUN: -analyzer-config optin.cplusplus.VirtualCall:ShowFixIts=true
4+
5+
struct S {
6+
virtual void foo();
7+
S() {
8+
foo();
9+
// expected-warning@-1 {{Call to virtual method 'S::foo' during construction bypasses virtual dispatch}}
10+
// CHECK-FIXES: S::foo();
11+
}
12+
~S();
13+
};

clang/test/lit.cfg.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@
7777
if config.clang_staticanalyzer_z3 == '1':
7878
config.available_features.add('z3')
7979

80+
check_analyzer_fixit_path = os.path.join(
81+
config.test_source_root, "Analysis", "check-analyzer-fixit.py")
82+
config.substitutions.append(
83+
('%check_analyzer_fixit',
84+
'%s %s' % (config.python_executable, check_analyzer_fixit_path)))
8085

8186
llvm_config.add_tool_substitutions(tools, tool_dirs)
8287

0 commit comments

Comments
 (0)