blob: 13e156672caae1e671b2804e0994f382f182ce0b [file] [edit]
#!/usr/bin/env python3
# Copyright (C) 2023-2025 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
import os
import re
import subprocess
import sys
WEBKIT_CHECKERS = [
'alpha.webkit.ForwardDeclChecker',
'alpha.webkit.MemoryUnsafeCastChecker',
'alpha.webkit.NoDeleteChecker',
'alpha.webkit.NoUncheckedPtrMemberChecker',
'alpha.webkit.NoUnretainedMemberChecker',
'alpha.webkit.RetainPtrCtorAdoptChecker',
'alpha.webkit.UncheckedCallArgsChecker',
'alpha.webkit.UncheckedLambdaCapturesChecker',
'alpha.webkit.UncheckedLocalVarsChecker',
'alpha.webkit.UncountedCallArgsChecker',
'alpha.webkit.UncountedLocalVarsChecker',
'alpha.webkit.UnretainedCallArgsChecker',
'alpha.webkit.UnretainedLambdaCapturesChecker',
'alpha.webkit.UnretainedLocalVarsChecker',
'webkit.NoUncountedMemberChecker',
'webkit.RefCntblBaseVirtualDtor',
'webkit.UncountedLambdaCapturesChecker',
]
def args_for_additional_checkers(checker_names):
if not len(checker_names):
return []
return ['-analyzer-checker', ','.join(checker_names)]
def make_analyzer_flags(output_dir, args):
additional_checkers = []
analyzer_flags = []
if args.disable_default_checkers:
# FIXME: Disable clang-tidy bugprone-* checkers with -analyzer-tidy-checker=-*
# when that works.
# See: <https://clang.llvm.org/docs/analyzer/checkers.html>
all_checker_categories = [
'alpha',
'apiModeling',
# 'bugprone-infinite-loop',
# 'bugprone-move-forwarding-reference',
'core',
'cplusplus',
'deadcode',
'debug',
'fuchsia',
'nullability',
'optin',
'osx',
'security',
'unix',
'webkit',
]
analyzer_flags.extend([
'-analyzer-disable-checker',
','.join(all_checker_categories),
])
else:
additional_checkers.extend([
'optin.cplusplus.UninitializedObject',
'optin.cplusplus.VirtualCall',
])
if args.disable_checkers:
delimiters = re.compile(r'[\s,]+')
checkers = [ value for value in delimiters.split(args.disable_checkers) if value ]
analyzer_flags.extend([
'-analyzer-disable-checker',
','.join(checkers),
])
if args.enable_webkit_checkers:
additional_checkers.extend(WEBKIT_CHECKERS)
if args.enable_checkers:
delimiters = re.compile(r'[\s,]+')
checkers = [ value for value in delimiters.split(args.enable_checkers) if value ]
additional_checkers.extend(checkers)
if args.only_smart_pointers:
additional_checkers.extend(WEBKIT_CHECKERS)
# FIXME: Remove this code once we've fully migrated to 2026-07-17 merge.
if args.toolchains:
result = subprocess.run(['xcrun', 'clang', '-cc1', '-analyzer-checker-help-alpha'], env={'TOOLCHAINS': args.toolchains}, capture_output=True, encoding='ascii')
# Remove checker if not available in the specified toolchain.
if 'alpha.webkit.UncheckedLambdaCapturesChecker' not in result.stdout:
additional_checkers.remove('alpha.webkit.UncheckedLambdaCapturesChecker')
if additional_checkers:
analyzer_flags.extend(args_for_additional_checkers(additional_checkers))
if args.analyzer_budget_max_nodes is not None:
analyzer_flags.extend([
'-analyzer-config',
'max-nodes={}'.format(args.analyzer_budget_max_nodes),
])
if args.only_smart_pointers:
analyzer_flags.extend([
'-analyzer-config',
'verbose-report-filename=true',
])
return ' '.join(analyzer_flags)
def scan_build_path(sdkroot):
"""
Try to find scan-build in the SDK, else assume scan-build is in the
user's PATH. It may be downloaded from the clang/llvm repository.
"""
try:
with open(os.devnull, 'w') as dev_null:
return str(subprocess.check_output(
['xcrun', '-find', '-sdk', sdkroot, 'scan-build'],
stderr=dev_null).rstrip().decode('utf-8'))
except subprocess.CalledProcessError:
# FIXME: Replace scan-build with our own script, or add a copy to WebKit.
return 'scan-build'
def main(args):
path_to_scan_build = scan_build_path(args.sdkroot)
if args.scan_build_path_arg:
path_to_scan_build = args.scan_build_path_arg
if not path_to_scan_build:
print('ERROR: Could not find path to scan-build.')
return 1
output_dir = os.path.realpath(args.output_dir)
analyzer_flags = make_analyzer_flags(output_dir, args)
analyzer_path = os.path.realpath(args.analyzer_path) if args.analyzer_path else None
os.environ['ANALYZER_FLAGS'] = analyzer_flags
os.environ['ANALYZER_OUTPUT'] = output_dir
if args.analyzer_path:
os.environ['ANALYZER_EXEC'] = analyzer_path
os.environ['PATH_TO_SCAN_BUILD'] = path_to_scan_build
print('PATH_TO_SCAN_BUILD="{}"'.format(os.environ['PATH_TO_SCAN_BUILD']))
generate_static_analysis_archive_path = os.path.realpath(
os.path.join(os.path.dirname(__file__), 'generate-static-analysis-archive'))
set_webkit_config_path = os.path.realpath(
os.path.join(os.path.dirname(__file__), 'set-webkit-configuration'))
make_command = ['make', 'analyze', 'SDKROOT={}'.format(args.sdkroot)]
if args.analyzer_path:
make_command.extend(['CC={}'.format(analyzer_path), 'CPLUSPLUS={}'.format(analyzer_path)])
if args.preprocessor_additions:
make_command.extend(['GCC_PREPROCESSOR_ADDITIONS={}'.format(args.preprocessor_additions)])
if args.scheme:
make_command.extend(['SCHEME={}'.format(args.scheme)])
xcodebuild_args = {}
if args.toolchains:
xcodebuild_args['TOOLCHAINS'] = args.toolchains
if args.swift_conditions:
xcodebuild_args['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = args.swift_conditions
xcodebuild_args['COMPILATION_CACHE_ENABLE_CACHING'] = 'YES' if args.enable_compile_cache else 'NO'
xcodebuild_args['CLANG_ENABLE_COMPILE_CACHE'] = 'YES' if args.enable_compile_cache else 'NO'
if len(xcodebuild_args):
make_command.extend(['ARGS={}'.format(' '.join(["{}=\"{}\"".format(key, xcodebuild_args[key]) for key in xcodebuild_args]))])
# FIXME (rdar://168692628): With Xcode compilation caching (CAS) disabled,
# clang's `-fbounds-safety` silently turns on
# `-fexperimental-late-parse-attributes` without it appearing on the command
# line, so it is absent from the explicit-module context hash. The
# `SwiftShims` PCM built for a bounds-safety C++-interop target (late-parse
# on) then collides in the module cache with the toolchain's own Swift
# interface compiles (`Cxx`, `_StringProcessing`; late-parse off)
if args.enable_experimental_late_parse_attributes:
make_command.append('OTHER_CFLAGS=-fexperimental-late-parse-attributes -fexperimental-bounds-safety-attributes -Wno-error=unsafe-buffer-usage')
make_command.append('OTHER_SWIFT_FLAGS=-Xcc -fexperimental-late-parse-attributes -Xcc -fexperimental-bounds-safety-attributes -Xcc -Wno-error=unsafe-buffer-usage')
commands = [
[set_webkit_config_path, '--{}'.format(args.configuration)],
make_command,
[generate_static_analysis_archive_path, '--count', '--output-root', output_dir, '--id-string', 'WebKit Build'],
]
if args.dry_run:
env_vars_to_show = ['ANALYZER_EXEC', 'ANALYZER_FLAGS', 'ANALYZER_OUTPUT']
print('\n'.join(f'{key}="{value}"' for key, value in os.environ.items() if key in env_vars_to_show))
for command in commands:
print('\n' + ' '.join(command))
return 0
# FIXME: Handle Ctrl-C interrupts gracefully so subprocess jobs actually stop.
for command in commands:
print('\n+ ' + ' '.join(command))
result = subprocess.run(command)
if result.returncode:
return result.returncode
return 0
def parse_args():
parser = argparse.ArgumentParser(
description='Run clang static analyzer via `make analyze` and generate an HTML report.')
parser.add_argument('--analyzer-budget-max-nodes', dest='analyzer_budget_max_nodes',
type=str, default=None,
help='Increase max-nodes budget for clang static analyzer.')
parser.add_argument('--analyzer-path', dest='analyzer_path',
type=str, default=None,
help='Override path to clang static analyzer in scan-build and set CC/CPLUSPLUS for Xcode.')
parser.add_argument('--toolchains',
type=str, default=None,
help='Override Xcode toolchain.')
parser.add_argument('--disable-checkers', dest='disable_checkers',
help='Disable specific checkers listed.')
parser.add_argument('--disable-default-checkers', dest='disable_default_checkers',
action='store_true', default=False,
help='Disable all checkers by default to run only specific checkers.')
parser.add_argument('--enable-webkit-checkers', dest='enable_webkit_checkers',
action='store_true', default=False,
help='Enable all WebKit-specific checkers.')
parser.add_argument('--enable-checkers', dest='enable_checkers',
help='Enable specific checkers listed.')
parser.add_argument('--only-smart-pointers', dest='only_smart_pointers',
action='store_true', default=False,
help='Enable only WebKit-specific checkers for Smart Pointers. '
'Implies --analyzer-budget-max-nodes=10000000 and --disable-default-checkers.')
parser.add_argument('--output-dir', dest='output_dir', required=True,
help='Output directory for HTML results.')
parser.add_argument('--sdkroot', dest='sdkroot', default='macosx',
help='SDKROOT to use (default: macosx).')
parser.add_argument('--scan-build-path', dest='scan_build_path_arg', default=None,
help='Path to scan-build for OpenSource.')
parser.add_argument('--scheme', dest='scheme', default=None,
help='Xcode scheme to use when performing Build and Analyze.')
parser.add_argument('--preprocessor-additions', default=None,
help='Additional preprocessor macros.')
parser.add_argument('--swift-conditions', default=None,
help='Additional Swift active compilation conditions.')
parser.add_argument('--dry-run', default=False,
action='store_true', help='Output the command instead of executing it.')
parser.add_argument('--configuration', default='debug', choices=['debug', 'release'], dest='configuration', help='Set the build configuration (default: debug).')
compile_cache_group = parser.add_mutually_exclusive_group()
compile_cache_group.add_argument('--enable-compile-cache', dest='enable_compile_cache',
action='store_true', default=False,
help='Enable Xcode compilation caching (CAS).')
compile_cache_group.add_argument('--disable-compile-cache', dest='enable_compile_cache',
action='store_false', default=False,
help='Disable Xcode compilation caching (CAS) (default).')
parser.add_argument('--enable-experimental-late-parse-attributes', dest='enable_experimental_late_parse_attributes',
action='store_true', default=False,
help='Pass -fexperimental-late-parse-attributes to clang and swift-frontend. Workaround for rdar://168692628 SwiftShims PCM mismatch when compilation caching is disabled')
args = parser.parse_args()
if args.analyzer_path and not os.path.isfile(os.path.realpath(args.analyzer_path)):
parser.error('Analyzer path does not exist: \'{}\''.format(args.analyzer_path))
if args.enable_webkit_checkers and args.only_smart_pointers:
parser.error('Can\'t use --enable-webkit-checkers and --only-smart-pointers together.')
if args.only_smart_pointers:
args.analyzer_budget_max_nodes = '10000000'
args.disable_default_checkers = True
return args
if __name__ == '__main__':
sys.exit(main(parse_args()))