diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000..9436d1342 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,2 @@ +common --noenable_bzlmod +build --swiftcopt=-whole-module-optimization diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 000000000..19b860c18 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +6.4.0 diff --git a/.gitignore b/.gitignore index 4b986841a..bc79b5f83 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ DerivedData .swiftpm # VSCode -.vscode/* \ No newline at end of file +.vscode/* + +bazel-* diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 000000000..00c44603d --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,40 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "build_bazel_rules_swift", + sha256 = "9919ed1d8dae509645bfd380537ae6501528d8de971caebed6d5185b9970dc4d", + url = "https://github.com/bazelbuild/rules_swift/releases/download/2.1.1/rules_swift.2.1.1.tar.gz", +) + +load( + "@build_bazel_rules_swift//swift:repositories.bzl", + "swift_rules_dependencies", +) + +swift_rules_dependencies() + +load( + "@build_bazel_rules_swift//swift:extras.bzl", + "swift_rules_extra_dependencies", +) + +swift_rules_extra_dependencies() + +PERIPHERY_VERSION = "2.21.0" + +http_archive( + name = "com_github_peripheryapp_periphery", + build_file_content = """ +load("@bazel_skylib//rules:native_binary.bzl", "native_binary") + +native_binary( +name = "periphery_tool", +src = "periphery", +out = "periphery_tool-bazel", +visibility = ["//visibility:public"], +) + """, + sha256 = "7ea2b48e3444609c83dd642a8eeff7240316bf081d331e4ca91eeedb439c0668", + type = "zip", + url = "https://github.com/peripheryapp/periphery/releases/download/{version}/periphery-{version}.zip".format(version = PERIPHERY_VERSION), +) diff --git a/periphery/BUILD b/periphery/BUILD new file mode 100644 index 000000000..e69de29bb diff --git a/periphery/collect_periphery_info.bzl b/periphery/collect_periphery_info.bzl new file mode 100644 index 000000000..032a9cc3f --- /dev/null +++ b/periphery/collect_periphery_info.bzl @@ -0,0 +1,65 @@ +load("@build_bazel_rules_swift//swift:providers.bzl", "SwiftInfo") + +PeripheryInfo = provider( + doc = "Provides indexstore information for a target's recursive dependencies.", + fields = { + "periphery_file_target_mapping": "A File listing the sources of a target and paths to their respective indexstores.", + "periphery_indexstore": "A File representing the indexstore of a target.", + "srcs": "A list of sources for a target.", + }, +) + +def _collect_periphery_info_aspect_imp(target, ctx): + periphery_file_target_mapping = [] + periphery_indexstore = [] + srcs = [] + + # Only act on SwiftInfo targets that are not testonly, and exist in the current workspace + if SwiftInfo in target and not ctx.rule.attr.testonly and not target.label.workspace_name and hasattr(target[SwiftInfo], "direct_modules"): + for module in target[SwiftInfo].direct_modules: + if hasattr(module, "swift") and hasattr(module.swift, "indexstore") and module.swift.indexstore: + periphery_file_target_mappings = ctx.actions.declare_file("{}_periphery_file_target_mappings.json".format(module.name)) + swift_srcs = [src for src in module.compilation_context.direct_sources if src.extension == "swift" and src.is_source] + infoplist_srcs = [file for file in ctx.rule.files.data if file.extension == "plist"] + ctx.actions.write( + output = periphery_file_target_mappings, + content = json.encode(_create_file_target_info(swift_srcs + infoplist_srcs, module.name)), + ) + periphery_file_target_mapping.append(periphery_file_target_mappings) + periphery_indexstore.append(module.swift.indexstore) + srcs.extend(swift_srcs + infoplist_srcs) + periphery_file_target_mapping_depset = depset( + direct = periphery_file_target_mapping, + transitive = [dep[PeripheryInfo].periphery_file_target_mapping for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + periphery_indexstore_depset = depset( + direct = periphery_indexstore, + transitive = [dep[PeripheryInfo].periphery_indexstore for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + srcs_depset = depset( + direct = srcs, + transitive = [dep[PeripheryInfo].srcs for dep in ctx.rule.attr.deps] if hasattr(ctx.rule.attr, "deps") else [], + ) + return [ + PeripheryInfo( + periphery_file_target_mapping = periphery_file_target_mapping_depset, + periphery_indexstore = periphery_indexstore_depset, + srcs = srcs_depset, + ), + OutputGroupInfo( + periphery_file_target_mapping = periphery_file_target_mapping_depset, + periphery_indexstore = periphery_indexstore_depset, + srcs = srcs_depset, + ), + ] + +def _create_file_target_info(srcs, module_name): + info = {} + for src in srcs: + info[src.path] = [module_name] + return {"file_targets": info} + +collect_periphery_info_aspect = aspect( + implementation = _collect_periphery_info_aspect_imp, + attr_aspects = ["deps"], +) diff --git a/periphery/example/BUILD b/periphery/example/BUILD new file mode 100644 index 000000000..0c4b0b8fd --- /dev/null +++ b/periphery/example/BUILD @@ -0,0 +1,13 @@ +load( + "//periphery:periphery_report.bzl", + "periphery_report", +) + +periphery_report( + name = "periphery_xcode_report", + format = "xcode", + periphery_tool = "@com_github_peripheryapp_periphery//:periphery_tool", + deps = [ + "//periphery/example/src:ReportTest", + ], +) diff --git a/periphery/example/src/BUILD b/periphery/example/src/BUILD new file mode 100644 index 000000000..a429636c7 --- /dev/null +++ b/periphery/example/src/BUILD @@ -0,0 +1,15 @@ +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +swift_library( + name = "ReportTest", + srcs = [ + "periphery_test.swift", + ], + data = [ + "Info.plist", + ], + visibility = ["//periphery/example:__pkg__"], +) diff --git a/periphery/example/src/Info.plist b/periphery/example/src/Info.plist new file mode 100644 index 000000000..934563a71 --- /dev/null +++ b/periphery/example/src/Info.plist @@ -0,0 +1,8 @@ + + + + + NSPrincipalClass + FixturePrincipleClass + + diff --git a/periphery/example/src/periphery_test.swift b/periphery/example/src/periphery_test.swift new file mode 100644 index 000000000..b8def76da --- /dev/null +++ b/periphery/example/src/periphery_test.swift @@ -0,0 +1,30 @@ +import Foundation + +protocol FixtureProtocol83: AnyObject { + func protocolMethod() +} + +extension FixtureProtocol83 { + func protocolMethod() {} +} + +class FixtureClass83: FixtureProtocol83 {} + +class FixtureClass84: FixtureClass83 { + func protocolMethod() {} +} + +public class FixtureClass85 { + private let cls: FixtureClass84 + weak var delegate: FixtureProtocol83? + + init() { + cls = FixtureClass84() + } + + public func someMethod() { + delegate?.protocolMethod() + } +} + +public class FixturePrincipleClass {} diff --git a/periphery/periphery_report.bzl b/periphery/periphery_report.bzl new file mode 100644 index 000000000..b7ca6f2a4 --- /dev/null +++ b/periphery/periphery_report.bzl @@ -0,0 +1,149 @@ +load( + ":collect_periphery_info.bzl", + "PeripheryInfo", + "collect_periphery_info_aspect", +) + +def _force_indexstore_impl(settings, _attr): + return { + "//command_line_option:features": settings["//command_line_option:features"] + [ + "swift.index_while_building", + ], + } + +_force_indexstore = transition( + implementation = _force_indexstore_impl, + inputs = [ + "//command_line_option:features", + ], + outputs = [ + "//command_line_option:features", + ], +) + +PeripheryReportInfo = provider( + doc = "Provides periphery report information for usage by other targets.", + fields = { + "report": "A File containing a periphery report.", + }, +) + +def _periphery_report_impl(ctx): + periphery_file_inputs = _collect_file_inputs(ctx) + args = ctx.actions.args() + args.add_all([ + "--format=%s" % ctx.attr.format, + "--skip-build", + "--relative-results", + "--quiet", + ] + ctx.attr.periphery_additonal_args) + if ctx.attr.report_exclude_globs: + args.add("--report-exclude") + for glob in ctx.attr.report_exclude_globs: + args.add(glob) + config_file_output = ctx.actions.declare_file("periphery_config.yml") + ctx.actions.write( + output = config_file_output, + content = """ +file_targets_path: +- {file_targets_paths} +index_store_path: +- {index_store_paths} +""".format( + file_targets_paths = "\n- ".join([f.path for f in periphery_file_inputs.periphery_file_target_mapping_files]), + index_store_paths = "\n- ".join([f.path for f in periphery_file_inputs.periphery_indexstore_files]), + ), + ) + args.add_all(["--config", config_file_output.path]) + extension = ctx.attr.format if ctx.attr.format == "json" else "txt" + output_file = ctx.actions.declare_file(ctx.label.name + "_periphery_report.%s" % extension) + ctx.actions.run_shell( + tools = [ + ctx.executable.periphery_tool, + config_file_output, + ] + periphery_file_inputs.runfiles.files.to_list(), + arguments = [args], + outputs = [output_file], + command = "{executable} scan $@ > {output_path}".format( + executable = ctx.executable.periphery_tool.path, + output_path = output_file.path, + ), + mnemonic = "GeneratePeripheryReport", + ) + return [ + DefaultInfo( + files = depset([output_file]), + runfiles = periphery_file_inputs.runfiles, + ), + PeripheryReportInfo( + report = output_file, + ), + ] + +def _collect_file_inputs(ctx): + runfiles = ctx.runfiles( + files = [ + ctx.executable.periphery_tool, + ], + ) + periphery_file_target_mapping_files = [] + periphery_indexstore_files = [] + srcs_files = [] + for dep in ctx.attr.deps: + dep_runfiles = [] + periphery_file_target_mapping_runfiles = ctx.runfiles(transitive_files = dep[PeripheryInfo].periphery_file_target_mapping) + periphery_file_target_mapping_files.extend(dep[PeripheryInfo].periphery_file_target_mapping.to_list()) + dep_runfiles.append(periphery_file_target_mapping_runfiles) + periphery_indexstore_paths_depset = ctx.runfiles(transitive_files = dep[PeripheryInfo].periphery_indexstore) + periphery_indexstore_files.extend(dep[PeripheryInfo].periphery_indexstore.to_list()) + dep_runfiles.append(periphery_indexstore_paths_depset) + srcs_depset = ctx.runfiles(transitive_files = dep[PeripheryInfo].srcs) + srcs_files.extend(dep[PeripheryInfo].srcs.to_list()) + dep_runfiles.append(srcs_depset) + for dep_runfile in dep_runfiles: + runfiles = runfiles.merge(dep_runfile) + return struct( + runfiles = runfiles, + periphery_file_target_mapping_files = periphery_file_target_mapping_files, + periphery_indexstore_files = periphery_indexstore_files, + srcs_files = srcs_files, + ) + +periphery_report = rule( + implementation = _periphery_report_impl, + doc = "Creates a periphery report for the given targets.", + attrs = { + "deps": attr.label_list( + cfg = _force_indexstore, + aspects = [collect_periphery_info_aspect], + doc = "The targets to generate a periphery report from.", + ), + "report_exclude_globs": attr.string_list( + default = [], + doc = "A list of file globs to exclude from the report.", + ), + "periphery_additonal_args": attr.string_list( + doc = "A list additional arguments to pass to the periperhy invocation.", + ), + "format": attr.string( + default = "xcode", + values = [ + "xcode", + "json", + "csv", + "checkstyle", + "codeclimate", + "github-actions", + ], + doc = "The output format to use.", + ), + "periphery_tool": attr.label( + doc = "The periphery tool to use.", + executable = True, + cfg = "exec", + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, +)