Index: clang/include/clang/StaticAnalyzer/Checkers/Checkers.td =================================================================== --- clang/include/clang/StaticAnalyzer/Checkers/Checkers.td +++ clang/include/clang/StaticAnalyzer/Checkers/Checkers.td @@ -21,7 +21,7 @@ def Core : Package<"core">; def CoreBuiltin : Package<"builtin">, ParentPackage, Hidden; -def CoreUninitialized : Package<"uninitialized">, ParentPackage; +def CoreUninitialized : Package<"uninitialized">, ParentPackage; def CoreAlpha : Package<"core">, ParentPackage; // The OptIn package is for checkers that are not alpha and that would normally @@ -1501,3 +1501,15 @@ } // end fuchsia +//===----------------------------------------------------------------------===// +// Pure alpha checkers. +//===----------------------------------------------------------------------===// + +let ParentPackage = Alpha in { + +def ExampleChecker : Checker<"Example">, + HelpText<"FIXME">, + Documentation, + Hidden; + +} // end "alpha" Index: clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt =================================================================== --- clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt +++ clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt @@ -39,6 +39,7 @@ DynamicTypePropagation.cpp DynamicTypeChecker.cpp EnumCastOutOfRangeChecker.cpp + ExampleChecker.cpp ExprInspectionChecker.cpp FixedAddressChecker.cpp FuchsiaHandleChecker.cpp Index: clang/lib/StaticAnalyzer/Checkers/ExampleChecker.cpp =================================================================== --- /dev/null +++ clang/lib/StaticAnalyzer/Checkers/ExampleChecker.cpp @@ -0,0 +1,101 @@ +//===- ExampleChecker - Copy-pasted by 'add-new-checker.py' -----*- 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 +// +//===----------------------------------------------------------------------===// +// +// FIXME: Document 'ExampleChecker'. +// +//===----------------------------------------------------------------------===// + +#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/CheckerManager.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" + +using namespace clang; +using namespace ento; + +namespace { +class ExampleChecker : public Checker { +public: + void checkPostCall(const CallEvent &Call, CheckerContext &C) const; + +private: + BugType BT{this, "Insufficiently awesome function call", + categories::LogicError}; + // Example call description. Note: for multiple ones use CallDescriptionMap. + CallDescription MarkNotAwesome{{"std", "mark_not_awesome"}, 1}; +}; +} // namespace + +// Example state trait. Note: It is usual to map values to memory regions. +REGISTER_MAP_WITH_PROGRAMSTATE(AwesomeMap, const MemRegion *, bool) + +// Example subscription. Here we are interested in already called functions. +void ExampleChecker::checkPostCall(const CallEvent &Call, + CheckerContext &C) const { + ProgramStateRef State = C.getState(); + + // Check if the origin of 'Call' is not awesome, if so emit a bug report. + // 'dyn_cast_or_null' (or cast_or_null) on nullable. + if (const CallExpr *CE = dyn_cast_or_null(Call.getOriginExpr())) { + if (const auto *FD = dyn_cast(CE->getCalleeDecl())) { + // SVal is a simple value type. Note: Values transcends time and space, + // therefore we could ask the Analyzer engine to give a name for any + // kind of value using the SValBuilder. + SVal FunctionV = C.getSValBuilder().getFunctionPointer(FD); + const MemRegion *MR = FunctionV.getAsRegion(); + // State traits returns 'const *' + if (const bool *Entry = State->get(MR)) { + bool IsNotAwesome = *Entry; + if (!IsNotAwesome) + return; + + // Usual out-stream boilerplate. + SmallString<128> Msg; + llvm::raw_svector_ostream Out(Msg); + + // 'cast' on known types. + Out << "There is a path where '" << cast(FD)->getName() + << "' is insufficiently awesome"; + + // Usual bug-reporting boilerplate. + auto Report = std::make_unique( + BT, Out.str(), C.generateErrorNode()); + C.emitReport(std::move(Report)); + } + } + } + + // Check if 'Call' is 'mark_not_awesome()', if so emit a helper report. + // Note: These reports helps the user to understand how a bug could happen. + if (Call.isCalled(MarkNotAwesome)) { + const MemRegion *MR = Call.getArgSVal(0).getAsRegion(); + State = State->set(MR, true); + + // NoteTags are emitted on the fly during analysis with this callback. + // Here we emit a note on the event when the function became not awesome. + // Note: for reverse-engineering such notes use BugReporterVisitor. + const NoteTag *Tag = C.getNoteTag([=]() -> std::string { + // Usual boilerplate for cheap string concatenation. + return (Twine("Function '") + + cast(MR)->getDecl()->getName() + + "' marked as not awesome") + .str(); + }); + + // Emit the NoteTag. + C.addTransition(State, Tag); + } +} + +void ento::registerExampleChecker(CheckerManager &Mgr) { + Mgr.registerChecker(); +} + +bool ento::shouldRegisterExampleChecker(const LangOptions &) { return true; } Index: clang/test/Analysis/add-new-checker/add-main-package.rst =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/add-main-package.rst @@ -0,0 +1,52 @@ +.. RUN: %check_add_new_checker %s %t b.A +.. RUN: %check_add_new_checker %s %t alpha.b.A + +.. _default-checkers: + +Default Checkers +---------------- + +.. _core-checkers: + +core +^^^^ +Foo. + +.. _core-z: + +core.z.Z (C) +"""""""""""" +Bar. + +.. CHECK-B-A: Bar. +.. CHECK-B-A-EMPTY: +.. CHECK-B-A-NEXT: b +.. CHECK-B-A-NEXT: ^ +.. CHECK-B-A-NEXT: FIXME: Document 'b'. +.. CHECK-B-A-EMPTY: +.. CHECK-B-A-NEXT: .. _b-A: +.. CHECK-B-A-EMPTY: +.. CHECK-B-A-NEXT: b.AChecker (C) +.. CHECK-B-A-EMPTY: +.. CHECK-B-A-NEXT: .. _alpha-checkers: +.. _alpha-checkers: + +Experimental Checkers +--------------------- +Baz. + +.. _alpha-z: + +alpha.z.Z (C) +""""""""""""" +Qux. + +.. CHECK-ALPHA-B-A: Qux. +.. CHECK-ALPHA-B-A-EMPTY: +.. CHECK-ALPHA-B-A-NEXT: alpha.b +.. CHECK-ALPHA-B-A-NEXT: ^^^^^^^ +.. CHECK-ALPHA-B-A-NEXT: FIXME: Document 'alpha.b'. +.. CHECK-ALPHA-B-A-EMPTY: +.. CHECK-ALPHA-B-A-NEXT: .. _alpha-b-A: +.. CHECK-ALPHA-B-A-EMPTY: +.. CHECK-ALPHA-B-A-NEXT: alpha.b.AChecker (C) Index: clang/test/Analysis/add-new-checker/add-main-package.td =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/add-main-package.td @@ -0,0 +1,20 @@ +// RUN: %check_add_new_checker %s %t a.A + +include "CheckerBase.td" + +def Z : Package<"z">; + +// CHECK-A-A: def Z : Package<"z">; +// CHECK-A-A-NEXT: def A : Package<"a">; + +let ParentPackage = Z in { +def ZChecker : Checker<"Z">; +} // end "z" + +// CHECK-A-A: } // end "z" +// CHECK-A-A-EMPTY: +// CHECK-A-A-NEXT: let ParentPackage = A in { +// CHECK-A-A-EMPTY: +// CHECK-A-A-NEXT: def AChecker : Checker<"A">, +// CHECK-A-A-EMPTY: +// CHECK-A-A-NEXT: } // end "a" Index: clang/test/Analysis/add-new-checker/add-multiple-packages.rst =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/add-multiple-packages.rst @@ -0,0 +1,24 @@ +.. RUN: %check_add_new_checker %s %t core.a.b.c.A + +.. _default-checkers: + +Default Checkers +---------------- + +.. _core-checkers: + +core +^^^^ +Foo. + +.. _core-a-A: + +core.a.A (C) +"""""""""""" +Bar. + +.. CHECK-CORE-A-B-C-A: Bar. +.. CHECK-CORE-A-B-C-A-EMPTY: +.. CHECK-CORE-A-B-C-A-NEXT: .. _core-a-b-c-A: +.. CHECK-CORE-A-B-C-A-EMPTY: +.. CHECK-CORE-A-B-C-A-NEXT: core.a.b.c.AChecker (C) Index: clang/test/Analysis/add-new-checker/add-multiple-packages.td =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/add-multiple-packages.td @@ -0,0 +1,26 @@ +// RUN: %check_add_new_checker %s %t a.b.c.A + +include "CheckerBase.td" + +def A : Package<"a">; +def B : Package<"b">; +// CHECK-A-B-C-A: def A : Package<"a">; +// CHECK-A-B-C-A-NEXT: def B : Package<"b">; +// CHECK-A-B-C-A-NEXT: def BA : Package<"b">, ParentPackage; +// CHECK-A-B-C-A-NEXT: def CBA : Package<"c">, ParentPackage; + +let ParentPackage = A in { +def AChecker : Checker<"A">; +} // end "a" + +let ParentPackage = B in { +def BChecker : Checker<"B">; +} // end "b" + +// CHECK-A-B-C-A: } // end "b" +// CHECK-A-B-C-A-EMPTY: +// CHECK-A-B-C-A-NEXT: let ParentPackage = CBA in { +// CHECK-A-B-C-A-EMPTY: +// CHECK-A-B-C-A-NEXT: def AChecker : Checker<"A">, +// CHECK-A-B-C-A-EMPTY: +// CHECK-A-B-C-A-NEXT: } // end "a.b.c" Index: clang/test/Analysis/add-new-checker/check-add-new-checker.py =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/check-add-new-checker.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +#===- check-add-new-checker.py - Static Analyzer test helper --*- python -*-===# +# +# 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 +# +#===------------------------------------------------------------------------===# +# +# Example usage: // RUN: %check_add_new_checker %s %t alpha.package.Foo +# +#===------------------------------------------------------------------------===# + + +import argparse +import os +import re +import subprocess + + +def run_test(args, extra_args): + input_file_path = args.input_file_path + temp_file_path = args.temp_file_path + + lines = [] + with open(input_file_path, 'r') as in_stream: + lines = in_stream.readlines() + + # Remove every 'CHECK' lines to make sure the test does not match on itself. + check_comment_re = re.compile('^..\sCHECK-') + addition = [] + for line in lines: + if not check_comment_re.match(line): + addition.append(line) + + # Dump out the test file: one is modifiable, one is the original. + with open(temp_file_path, 'w') as out_stream: + out_stream.writelines(addition) + + original_file_path = temp_file_path + ".orig" + with open(original_file_path, 'w') as out_stream: + out_stream.writelines(addition) + + clang_path = os.path.abspath(__file__) + clang_path = clang_path[:clang_path.find('clang') + 5] + add_new_checker_path = os.path.join( + clang_path, 'utils', 'analyzer', 'add-new-checker.py') + + add_new_checker = (['python', add_new_checker_path, '--test', + '--test-path', temp_file_path] + extra_args) + try: + add_new_checker_output = \ + subprocess.check_output(add_new_checker, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print('Adding a new checker failed:\n' + e.output.decode()) + raise + + try: + subprocess.check_output( + ['diff', '-u', original_file_path, temp_file_path], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print('Diff:\n' + e.output.decode()) + + # Build the 'CHECK' prefix based on the package-chain and the checker name. + prefixes = '' + for package_or_checker in extra_args[0].split('.'): + prefixes += package_or_checker.upper() + '-' + prefixes = prefixes[:-1] + + try: + subprocess.check_output( + ['FileCheck', '-input-file=' + temp_file_path, input_file_path, + '-check-prefixes=CHECK-' + prefixes, '-strict-whitespace'], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print('FileCheck failed:\n' + e.output.decode()) + print(add_new_checker_output) # Dump the script output as well. + raise + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('input_file_path') + parser.add_argument('temp_file_path') + + args, extra_args = parser.parse_known_args() + run_test(args, extra_args) + + +if __name__ == '__main__': + main() Index: clang/test/Analysis/add-new-checker/checker-name.rst =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/checker-name.rst @@ -0,0 +1,27 @@ +.. RUN: %check_add_new_checker %s %t core.a.Ab + +.. In ASCII 'b' < 'A', so that the checker names are compared in lowercase. + +.. _default-checkers: + +Default Checkers +---------------- + +.. _core-checkers: + +core +^^^^ +Foo. + +.. _core-a-AA: + +core.a.AA (C) +"""""""""""" +Bar. + +.. CHECK-CORE-A-AB: Bar. +.. CHECK-CORE-A-AB-EMPTY: +.. CHECK-CORE-A-AB-NEXT: .. _core-a-Ab: +.. CHECK-CORE-A-AB-EMPTY: +.. CHECK-CORE-A-AB-NEXT: core.a.AbChecker (C) +.. CHECK-CORE-A-AB-EMPTY: Index: clang/test/Analysis/add-new-checker/checker-name.td =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/checker-name.td @@ -0,0 +1,19 @@ +// RUN: %check_add_new_checker %s %t a.Ab + +include "CheckerBase.td" + +// In ASCII 'b' < 'A', so that the checker names are compared in lowercase. + +def A : Package<"a">; + +let ParentPackage = A in { + +def AAChecker : Checker<"AA">; + +// CHECK-A-AB: def AAChecker : Checker<"AA">; +// CHECK-A-AB-EMPTY: +// CHECK-A-AB-NEXT: def AbChecker : Checker<"Ab">, +// CHECK-A-AB-EMPTY: +// CHECK-A-AB-NEXT: } // end "a" +} // end "a" + Index: clang/test/Analysis/add-new-checker/flow-package-exist-checker.rst =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/flow-package-exist-checker.rst @@ -0,0 +1,44 @@ +.. RUN: %check_add_new_checker %s %t core.a.A +.. RUN: %check_add_new_checker %s %t core.a.C +.. RUN: %check_add_new_checker %s %t core.a.E + +.. _default-checkers: + +Default Checkers +---------------- + +.. _core-checkers: + +core +^^^^ +Foo. + +.. CHECK-CORE-A-A: .. _core-a-A: +.. CHECK-CORE-A-A-EMPTY: +.. CHECK-CORE-A-A-NEXT: core.a.AChecker (C) +.. CHECK-CORE-A-A-EMPTY: +.. CHECK-CORE-A-A-NEXT: .. _core-a-B: + +.. _core-a-B: + +core.a.B (C) +"""""""""""" +Bar. + +.. CHECK-CORE-A-C: .. _core-a-C: +.. CHECK-CORE-A-C-EMPTY: +.. CHECK-CORE-A-C-NEXT: core.a.CChecker (C) +.. CHECK-CORE-A-C-EMPTY: +.. CHECK-CORE-A-C-NEXT: .. _core-a-D: + +.. _core-a-D: + +core.a.D (C) +"""""""""""" +Baz. + +.. CHECK-CORE-A-E: Baz. +.. CHECK-CORE-A-E-EMPTY: +.. CHECK-CORE-A-E-NEXT: .. _core-a-E: +.. CHECK-CORE-A-E-EMPTY: +.. CHECK-CORE-A-E-NEXT: core.a.EChecker (C) Index: clang/test/Analysis/add-new-checker/flow-package-exist-checker.td =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/flow-package-exist-checker.td @@ -0,0 +1,31 @@ +// RUN: %check_add_new_checker %s %t a.A +// RUN: %check_add_new_checker %s %t a.C +// RUN: %check_add_new_checker %s %t a.E + +include "CheckerBase.td" + +def A : Package<"a">; + +let ParentPackage = A in { + +// CHECK-A-A: let ParentPackage = A in { +// CHECK-A-A-EMPTY: +// CHECK-A-A-NEXT: def AChecker : Checker<"A">, +// CHECK-A-A-EMPTY: +// CHECK-A-A-NEXT: def BChecker : Checker<"B">; +def BChecker : Checker<"B">; + +// CHECK-A-C: def BChecker : Checker<"B">; +// CHECK-A-C-EMPTY: +// CHECK-A-C-NEXT: def CChecker : Checker<"C">, +// CHECK-A-C-EMPTY: +// CHECK-A-C-NEXT: def DChecker : Checker<"D">; +def DChecker : Checker<"D">; + +// CHECK-A-E: def DChecker : Checker<"D">; +// CHECK-A-E-EMPTY: +// CHECK-A-E: def EChecker : Checker<"E">, +// CHECK-A-E-EMPTY: +// CHECK-A-E-NEXT: } // end "a" +} // end "a" + Index: clang/test/Analysis/add-new-checker/flow-package-not-exist.rst =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/flow-package-not-exist.rst @@ -0,0 +1,28 @@ +.. RUN: %check_add_new_checker %s %t core.b.A + +.. _default-checkers: + +Default Checkers +---------------- + +.. _core-checkers: + +core +^^^^ +Foo. + +.. _core-c-A: + +core.c.A (C) +"""""""""""" +Bar. + +.. CHECK-CORE-B-A: Bar +.. CHECK-CORE-B-A-EMPTY: +.. CHECK-CORE-B-A-NEXT: core.b +.. CHECK-CORE-B-A-NEXT: ^^^^^^ +.. CHECK-CORE-B-A-NEXT: FIXME: Document 'core.b'. +.. CHECK-CORE-B-A-EMPTY: +.. CHECK-CORE-B-A-NEXT: _core-b-A: +.. CHECK-CORE-B-A-EMPTY: +.. CHECK-CORE-B-A-NEXT: core.b.AChecker (C) Index: clang/test/Analysis/add-new-checker/flow-package-not-exist.td =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/flow-package-not-exist.td @@ -0,0 +1,39 @@ +// RUN: %check_add_new_checker %s %t a.b.B +// RUN: %check_add_new_checker %s %t a.d.D + +include "CheckerBase.td" + +def A : Package<"a">; +// CHECK-A-B-B: def A : Package<"a">; +// CHECK-A-B-B-NEXT: def BA : Package<"b">, ParentPackage; +// CHECK-A-B-B-NEXT: def CA : Package<"c">, ParentPackage; +def CA : Package<"c">, ParentPackage; +// CHECK-A-D-D: def CA : Package<"c">, ParentPackage; +// CHECK-A-D-D-NEXT: def DA : Package<"d">, ParentPackage; +// CHECK-A-D-D-NEXT: def EA : Package<"e">, ParentPackage; +def EA : Package<"e">, ParentPackage; + +//===----------------------------------------------------------------------===// + +let ParentPackage = CA in { +def CChecker : Checker<"C">; +} // end "a.c" + +let ParentPackage = EA in { +def EChecker : Checker<"E">; +} // end "a.e" +// CHECK-A-B-B: } // end "a.e" +// CHECK-A-B-B-EMPTY: +// CHECK-A-B-B-NEXT: let ParentPackage = BA in { +// CHECK-A-B-B-EMPTY: +// CHECK-A-B-B-NEXT: def BChecker : Checker<"B">, +// CHECK-A-B-B-EMPTY: +// CHECK-A-B-B-NEXT: } // end "a.b" + +// CHECK-A-D-D: } // end "a.e" +// CHECK-A-D-D-EMPTY: +// CHECK-A-D-D-NEXT: let ParentPackage = DA in { +// CHECK-A-D-D-EMPTY: +// CHECK-A-D-D-NEXT: def DChecker : Checker<"D">, +// CHECK-A-D-D-EMPTY: +// CHECK-A-D-D-NEXT: } // end "a.d" Index: clang/test/Analysis/add-new-checker/lit.local.cfg.py =================================================================== --- /dev/null +++ clang/test/Analysis/add-new-checker/lit.local.cfg.py @@ -0,0 +1,33 @@ +import lit.util +import lit.formats +import os + +# Choose between lit's internal shell pipeline runner and a real shell. If +# LIT_USE_INTERNAL_SHELL is in the environment, we use that as an override. +use_lit_shell = os.environ.get("LIT_USE_INTERNAL_SHELL") +if use_lit_shell: + # 0 is external, "" is default, and everything else is internal. + execute_external = (use_lit_shell == "0") +else: + # Otherwise we default to internal on Windows and external elsewhere, as + # bash on Windows is usually very slow. + execute_external = (not sys.platform in ['win32']) + +config.test_format = lit.formats.ShTest(execute_external) + +config.suffixes = ['.td', '.rst'] + +check_add_new_checker_path = \ + os.path.join(os.path.abspath(os.path.join(__file__, os.pardir)), + 'check-add-new-checker.py') + +config.substitutions.append(('%check_add_new_checker', + '%s %s' % (config.python_executable, + check_add_new_checker_path))) + +add_new_checker_path = os.path.join(config.clang_src_dir, 'utils', 'analyzer', + 'add-new-checker.py') + +config.substitutions.append(('%add_new_checker', + '%s %s' % (config.python_executable, + add_new_checker_path))) Index: clang/test/Analysis/example-checker.cpp =================================================================== --- /dev/null +++ clang/test/Analysis/example-checker.cpp @@ -0,0 +1,37 @@ +// RUN: %clang_analyze_cc1 \ +// RUN: -analyzer-checker=core,alpha.Example \ +// RUN: -analyzer-output=text -verify %s + +namespace std { +void mark_not_awesome(void()); +} // namespace std + +// FIXME: Add positive tests. +namespace test_bad { +void f(); +bool lunarPhase(); +void test() { + if (lunarPhase()) { + // expected-note@-1 {{Assuming the condition is true}} + // expected-note@-2 {{Taking true branch}} + std::mark_not_awesome(f); + // expected-note@-1 {{Function 'f' marked as not awesome}} + } + + f(); + // expected-note@-1 {{There is a path where 'f' is insufficiently awesome}} + // expected-warning@-2 {{There is a path where 'f' is insufficiently awesome}} +} +} // namespace test_bad + +// FIXME: Add negative tests. +namespace test_good { +void f(); +void test() { + { + void f(); + std::mark_not_awesome(f); + } + f(); // no-warning +} +} // namespace test_good Index: clang/utils/analyzer/add-new-checker.py =================================================================== --- /dev/null +++ clang/utils/analyzer/add-new-checker.py @@ -0,0 +1,637 @@ +#!/usr/bin/env python +# +#===- add-new-checker.py - Creates a Static Analyzer checker --*- python -*-===# +# +# 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 +# +#===------------------------------------------------------------------------===# +# +# Example usage: python add-new-checker.py alpha.package.Foo +# +#===------------------------------------------------------------------------===# + +import argparse +import json +import os +import re +import subprocess + + +class Checker: + def __init__(self, packages, name): + self.packages = packages + self.name = name + + def name(self): + return self.name + + def packages(self): + return self.packages + + def __str__(self): + return '.'.join(self.packages) + '.' + self.name + + +# Obtain the longest known package-chain of the 'packages' chain. For example +# the checker creation of 'alpha.core.a.Checker' would return ['alpha', 'core']. +def get_best_package_match(packages, known_packages): + best_package_match = [] + for i in range(len(packages)): + temp_packages = packages[:i + 1] + package_name = '.'.join(temp_packages) + if package_name in known_packages: + best_package_match = temp_packages + return best_package_match + + +# Put every additional package and the checker into an increasing size jagged +# array. For example the checker creation of 'core.a.Checker' would return +# '[['core', 'a'], ['core', 'a', 'Checker']]'. +def get_addition(packages, name, best_package_match): + addition = [] + match_count = len(best_package_match) + for i, package in enumerate(packages): + if i < match_count: + continue + addition.append(best_package_match + packages[match_count : i + 1]) + addition.append(packages + [name]) + return addition + + +def add_checkers_entry(clang_path, checker, args): + checkers_include_path = os.path.join(clang_path, + 'include', 'clang', 'StaticAnalyzer', 'Checkers') + checkers_path = os.path.join(checkers_include_path, 'Checkers.td') + + # This only could test '.td'. + if args.is_test: + checkers_path = args.test_path + if not checkers_path.endswith('.td.tmp'): + return + + packages = checker.packages + name = checker.name + + # See if TableGen is found. + try: + devnull = open(os.devnull) + subprocess.Popen([args.tblgen_path, '-h'], stdout=devnull, + stderr=devnull).communicate() + except OSError as e: + print("[error] llvm-tblgen is not found. Please specify it explicitly. " + "For example: '--tblgen-path llvm-tblgen'") + raise + + # Load the checkers' TableGen. + data = None + try: + data = subprocess.check_output([args.tblgen_path, '-dump-json', + checkers_path, + '-I=' + checkers_include_path]) + except subprocess.CalledProcessError as e: + print('TableGen JSON dump failed:\n' + e.output.decode()) + raise + + data = json.loads(data) + + # Store the package names with their chained definition. For example the + # package 'CoreAlpha' => 'alpha.core'. + package_name_def_map = {} + for package_def in data['!instanceof']['Package']: + package = data[package_def] + if package['ParentPackage']: + chain = [package] + while True: + parent_package = chain[-1]['ParentPackage'] + if parent_package is None: + break + chain.append(data[parent_package['def']]) + + dump = [] + for elem in reversed(chain): + package_name = elem['PackageName'] + dump.append(package_name) + package_name_def_map['.'.join(dump)] = package['!name'] + else: + package_name = package['PackageName'] + package_name_def_map[package_name] = package['!name'] + + # Store the opposite mapping of the above map as well. + package_def_name_map = {} + for tmp_name, definition in package_name_def_map.items(): + package_def_name_map[definition] = tmp_name + + checker_names = data['!instanceof']['Checker'] + parent_packages = data['!instanceof']['Package'] + + # See whether the new checker is already defined. + full_name = name + 'Checker' + packages_str = '.'.join(packages) + for temp_name in checker_names: + temp_parent_package_def = data[temp_name]['ParentPackage']['def'] + temp_package = package_def_name_map[temp_parent_package_def] + if full_name == temp_name and temp_package == packages_str: + print("[error] '" + full_name + "' exist in package '" + + packages_str + "'.") + exit() + + # Store the packages which has checkers. + package_defs_in_use = set() + for temp_name in checker_names: + package_defs_in_use.add(data[temp_name]['ParentPackage']['def']) + + # Store every additional package and the checker we need to create. + best_package_match = get_best_package_match(packages, package_name_def_map) + addition = get_addition(packages, name, best_package_match) + + # This definition increases if 'write_package_def()' fires. The idea is that + # it stores the last written package definition so we could chain new + # packages to it. + packages_def = package_name_def_map[packages_str] \ + if packages_str in package_name_def_map else '' + + def write_package_def(): + for add_packages in addition[:-1]: + temp_package = add_packages[-1] + if len(add_packages) == 1: # Main package. + temp_packages_def = temp_package.capitalize() + out_stream.write('def %s : Package<"%s">;\n' + % (temp_packages_def, temp_package)) + + # Update the map with the last known new definition. + packages_def = temp_packages_def + package_name_def_map[temp_package] = packages_def + continue + + # Normal package + parent_package = '.'.join(add_packages[:-1]) + parent_package_def = package_name_def_map[parent_package] + + # Update the map with the last known new definition. + packages_def = temp_package.capitalize() + parent_package_def + package_name_def_map['.'.join(add_packages)] = packages_def + + out_stream.write('def %s : Package<%s>, ParentPackage<%s>;\n' + % (packages_def, '"' + temp_package + '"', + parent_package_def)) + + def write_checker(): + out_stream.write('def %sChecker : Checker<"%s">,' % (name, name)) + if args.is_test: + out_stream.write('\n\n') + return + out_stream.write('\n HelpText<"FIXME">,') + out_stream.write('\n Documentation;\n\n') + + def write_package(): + all_packages = '.'.join(addition[-1][:-1]) + all_packages_def = package_name_def_map[all_packages] + + out_stream.write('let ParentPackage = %s in {\n\n' % all_packages_def) + write_checker() + out_stream.write('} // end "%s"\n' % all_packages) + + look_for_package = '.'.join(best_package_match) + look_for_package_def = package_name_def_map[look_for_package] \ + if len(best_package_match) > 0 else '' + is_parent_exist = look_for_package_def in parent_packages + + package_def_str = 'def (.*) : Package<"(.*)">'; + package_def_re = re.compile(package_def_str + ';$') + parent_package_def_re = \ + re.compile(package_def_str + ', ParentPackage<(.*)>;$') + package_or_parent_package_def_re = re.compile(package_def_str) + parent_package_let_re = re.compile('^let ParentPackage = (.*) in {$') + checker_def_re = re.compile('def (.*) : Checker<.*>') + + # Prefetch the line number of the end of the package definitions. + lines = [] + last_package_def_line_index = 0 + with open(checkers_path, 'r') as in_stream: + for i, line in enumerate(in_stream): + lines.append(line) + if package_or_parent_package_def_re.match(line): + last_package_def_line_index = i + + + # If the entire package declared add the checker inside its declaration. + if packages_def in package_defs_in_use: + with open(checkers_path, 'w') as out_stream: + is_checker_added = False + is_parent_package_let_found = False + + for line in lines: + if is_checker_added: + out_stream.write(line) + continue + + # Find the parent declaration. + result = parent_package_let_re.match(line) + if not is_parent_package_let_found and result: + temp_parent_def = result.group(1) + if packages_def == temp_parent_def: + is_parent_package_let_found = True + + # After that point we want to add the checker. + if not is_parent_package_let_found: + out_stream.write(line) + continue + + # Try to add the checker inside its parent package. + result = checker_def_re.match(line) + if result: + temp_name = result.group(1) + if temp_name.lower() > name.lower(): + write_checker() + is_checker_added = True + + # We are at the end of the parent package, add here. + if line.startswith('}'): + write_checker() + is_checker_added = True + + out_stream.write(line) + return + + + # If a new main package requested add its definition to the end of the + # definitions and create the package declaration at the end of the file. + if look_for_package_def not in parent_packages: + with open(checkers_path, 'w') as out_stream: + for i, line in enumerate(lines): + out_stream.write(line) + if i == last_package_def_line_index: + write_package_def() + + write_package() + return + + + # Otherwise add the new package definition by alphabetical order and create + # the package at the end of the file so the user could adjust its position. + with open(checkers_path, 'w') as out_stream: + is_checker_added = False + is_package_def_added = False + is_parent_package_def_found = False + + for i, line in enumerate(lines): + if is_package_def_added: + out_stream.write(line) + continue + + # Find the place for the package definitions. + if (not is_package_def_added + and package_def_re.match(line) is not None): + is_parent_package_def_found = True + + # After that point we want to find the parent package definition. + if not is_parent_package_def_found: + out_stream.write(line) + continue + + # Find the parent definition. + if not is_package_def_added: + result = parent_package_def_re.match(line) + if result: + temp_parent_def = result.group(1) + temp_parent = package_def_name_map[temp_parent_def] + + if (temp_parent > packages_str + or not temp_parent.startswith(look_for_package)): + write_package_def() + is_package_def_added = True + + # Check for the end of package definitions. + if not is_package_def_added: + out_stream.write(line) + if i == last_package_def_line_index: + write_package_def() + is_package_def_added = True + continue + + out_stream.write(line) + + # End of file reached. + write_package() + + +def add_doc_entry(clang_path, checker, args): + doc_path = os.path.join(clang_path, 'docs', 'analyzer', 'checkers.rst') + + # This only could test '.rst'. + if args.is_test: + doc_path = args.test_path + if not doc_path.endswith('.rst.tmp'): + return + + name = checker.name + packages = checker.packages + packages_str = '.'.join(packages) + + # Normalize the package separation with dots. + def normalize(raw_package): + return '.'.join(raw_package.split('-')) if '-' in raw_package \ + else raw_package + + # With that we also catch the '.. _package-checkers:' references where we + # need to obtain the package name by removing the '-checkers' part. + # Note: Simplification, like '([a-z0-9]+([.-][a-z0-9]+)+)' does not work + # due to some Python mystery. + package_re_str = '^(|\.\.\s*_)([a-z0-9]+([.-][a-z0-9]+)+)' + package_re = re.compile(package_re_str) + checker_re = re.compile(package_re_str + '[:.-]([A-Z]\w*)') + + known_packages = set() + lines = [] + with open(doc_path, 'r') as in_stream: + for line in in_stream: + lines.append(line) + result = package_re.match(line) + if result: + temp_package = result.group(2) + if temp_package.endswith('-checkers'): + continue + known_packages.add(normalize(temp_package)) + + # Store every additional package and the checker we need to create. + best_package_match = get_best_package_match(packages, known_packages) + addition = get_addition(packages, name, best_package_match) + + # Try to add a new main package, then only add the largest package-chain + # with the checker. + def write_checker(): + temp_package = '.'.join(addition[-1][:-1]) + if len(addition[0]) == 1: # New main package. + out_stream.write(temp_package) + out_stream.write('\n' + '^' * len(temp_package)) + out_stream.write("\nFIXME: Document '%s'.\n\n" % temp_package) + + title = str(checker) + 'Checker (C)' + out_stream.write('.. _%s:\n' % '-'.join(addition[-1])) + out_stream.write('\n' + title) + if args.is_test: + out_stream.write('\n\n') + return + out_stream.write('\n' + '"' * len(title)) + out_stream.write("\nFIXME: Document '%s'.\n" % name) + out_stream.write('\n.. code-block:: %s\n' % 'c') + out_stream.write('\n void test() {\n // FIXME\n }\n\n') + + # If a new non-alpha package requested add it before the alpha packages or + # if an alpha package requested add it to the end of the file so that the + # user could adjust its position. + is_checker_added = False + if packages_str not in known_packages: + with open(doc_path, 'w') as out_stream: + if addition[0][0] == 'alpha': # Main package. + out_stream.writelines(lines) + write_checker() + return + + for line in lines: + if is_checker_added: + out_stream.write(line) + continue + + # Find the '.. _alpha-checkers:' reference. + result = package_re.match(line) + if result and result.group(2) == 'alpha-checkers': + write_checker() + is_checker_added = True + + out_stream.write(line) + + # End of file reached. + if not is_checker_added: + write_checker() + return + + # Otherwise look for the existing parent package and add a checker to it. + with open(doc_path, 'w') as out_stream: + look_for_package = '.'.join(best_package_match) + is_parent_package_found = False + + for line in lines: + if is_checker_added: + out_stream.write(line) + continue + + # Find the parent package reference. + result = package_re.match(line) + if not is_parent_package_found and result: + temp_package = normalize(result.group(2)) + if temp_package.endswith('checkers'): + temp_package = temp_package[:-9] + + if temp_package == packages_str: + is_parent_package_found = True + + # Past the parent package. + if (is_parent_package_found and + not temp_package.startswith(look_for_package)): + write_checker() + is_checker_added = True + out_stream.write(line) + continue + + if not is_parent_package_found: + out_stream.write(line) + continue + + # Try to add the checker. + result = checker_re.match(line) + if result: + temp_name = result.group(4) + if temp_name.lower() > name.lower(): + write_checker() + is_checker_added = True + + out_stream.write(line) + + # End of file reached. + if not is_checker_added: + write_checker() + + +def add_release_notes_entry(clang_path, checker): + release_notes_path = os.path.join(clang_path, 'docs', 'ReleaseNotes.rst') + name = checker.name + + # See whether we have already got some new checkers. + lines = [] + is_new_checker_found = False + new_checker_re = re.compile("^- New checker: :doc:`(.*)<") + + with open(release_notes_path, 'r') as in_stream: + for line in in_stream: + lines.append(line) + if not is_new_checker_found and new_checker_re.match(line): + is_new_checker_found = True + + with open(release_notes_path, 'w') as out_stream: + is_added = False + is_static_analyzer_line_passed = False + look_for_name = '-'.join(checker.packages) + '-' + name + + def write_checker(): + out_stream.write('- New checker: :doc:`%s<%s>`.\n' + % (checker, look_for_name)) + out_stream.write("\n FIXME: Document '%s'.\n\n" % name) + + for i, line in enumerate(lines): + if is_added: + out_stream.write(line) + continue + + if (not is_static_analyzer_line_passed + and lines[i - 2].strip() == 'Static Analyzer'): + is_static_analyzer_line_passed = True + + if not is_static_analyzer_line_passed: + out_stream.write(line) + continue + + if not is_new_checker_found: + out_stream.write(line) + write_checker() + is_added = True + continue + + if (is_new_checker_found and line.startswith('- ') + and not new_checker_re.match(line)): + write_checker() + is_added = True + + out_stream.write(line) + + +def add_cmake_entry(clang_path, checker): + cmake_lists_path = os.path.join(clang_path, 'lib', 'StaticAnalyzer', + 'Checkers', 'CMakeLists.txt') + name = checker.name + full_name = name + 'Checker.cpp' + + lines = [] + with open(cmake_lists_path, 'r') as in_stream: + lines = in_stream.readlines() + + with open(cmake_lists_path, 'w') as out_stream: + is_added = False + for raw_line in lines: + line = raw_line.strip() + if (not is_added and line.endswith('.cpp') + and line.lower() > name.lower()): + out_stream.write(' ' + full_name + '\n') + is_added = True + out_stream.write(raw_line) + + +def add_checker(clang_path, checker): + name = checker.name + 'Checker' + checkers_path = \ + os.path.join(clang_path, 'lib', 'StaticAnalyzer', 'Checkers') + checker_path = os.path.join(checkers_path, name + '.cpp') + example_checker_path = os.path.join(checkers_path, 'ExampleChecker.cpp') + + lines = [] + with open(example_checker_path, 'r') as in_stream: + lines = in_stream.readlines() + + with open(checker_path, 'w') as out_stream: + lhs = '//===- ' + name + ' - FIXME: Tiny doc ' + rhs = '-*- C++ -*-===//' + header = lhs + ('-' * (80 - len(lhs) - len(rhs))) + rhs + out_stream.write(header + '\n') + + # Skip the first line. + for line in lines[1:]: + out_stream.write(line.replace('ExampleChecker', name)) + + +def add_test(clang_path, checker): + name = checker.name + tests_path = os.path.join(clang_path, 'test', 'Analysis') + test_name = '' + for c in name: + if c.isupper(): + test_name += '-' + test_name += c.lower() + test_name = test_name.lstrip('-') + '-checker' + + test_path = os.path.join(tests_path, test_name + '.cpp') + example_test_path = os.path.join(tests_path, 'example-checker.cpp') + + lines = '' + with open(example_test_path, 'r') as in_stream: + lines = in_stream.read() + + with open(test_path, 'w') as out_stream: + out_stream.write(lines.replace('alpha.Example', str(checker))) + + +def main(): + parser = argparse.ArgumentParser(description='Creates a new checker.') + parser.add_argument('name', help='the full name of the checker with the ' + 'packages, for example: ' + 'alpha.package.Checker') + parser.add_argument('--tblgen-path', default='llvm-tblgen', + help='the path to llvm-tblgen') + parser.add_argument('--test', dest='is_test', + action='store_true', help=argparse.SUPPRESS) + parser.add_argument('--test-path', help=argparse.SUPPRESS) + args = parser.parse_args() + + raw_packages_and_name = args.name + if '.' not in raw_packages_and_name: + print('[error] No package found. Please specify it explicitly. ' + 'For example: alpha.package.Checker.') + exit() + + raw_checker_parts = raw_packages_and_name.split('.') + raw_name = raw_checker_parts[-1] + raw_package_names = raw_checker_parts[:-1] + + # We will add the 'Checker' suffix where appropriate. + name = raw_name[0].upper() + raw_name[1:] + if name != 'Checker' and name.lower().endswith('checker'): + name = name[:-7] + + packages_and_name = '' + for package in raw_package_names: + packages_and_name += package.lower() + '.' + packages_and_name += name + + clang_path = os.path.abspath(__file__) + clang_path = clang_path[:clang_path.rfind('clang') + 5] + + # See whether the file of the new checker is already made. + full_name = name + 'Checker.cpp' + checker_path = os.path.join(clang_path, 'lib', 'StaticAnalyzer', + 'Checkers', full_name) + if os.path.isfile(checker_path): + print("[error] '" + full_name + "' already exist.") + exit() + + checker = Checker(packages_and_name.split('.')[:-1], name) + print("Creating checker '" + str(checker) + "'...") + + add_checkers_entry(clang_path, checker, args) + add_doc_entry(clang_path, checker, args) + + # At the moment only 'Checkers.td' and 'checkers.rst' are tested. + if args.is_test: + return + + add_cmake_entry(clang_path, checker) + add_release_notes_entry(clang_path, checker) + add_checker(clang_path, checker) + add_test(clang_path, checker) + + # FIXME: Add resources to study checker-writing(?) + print('Done.') + + +if __name__ == '__main__': + main();