diff --git a/docs/docs/usage-guide/additional_configurations.md b/docs/docs/usage-guide/additional_configurations.md index 8d205865..98ed8472 100644 --- a/docs/docs/usage-guide/additional_configurations.md +++ b/docs/docs/usage-guide/additional_configurations.md @@ -250,3 +250,15 @@ Where the `ignore_pr_authors` is a list of usernames that you want to ignore. !!! note There is one specific case where bots will receive an automatic response - when they generated a PR with a _failed test_. In that case, the [`ci_feedback`](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) tool will be invoked. + +### Ignoring Generated Files by Language/Framework + +To automatically exclude files generated by specific languages or frameworks, you can add the following to your `configuration.toml` file: + +``` +[config] +ignore_language_framework = ['protobuf', ...] +``` + +You can view the list of auto-generated file patterns in `generated_code_ignore.toml`. +Files matching these glob patterns will be automatically excluded from PR Agent analysis. \ No newline at end of file diff --git a/pr_agent/algo/file_filter.py b/pr_agent/algo/file_filter.py index 79bb4d8e..109aede9 100644 --- a/pr_agent/algo/file_filter.py +++ b/pr_agent/algo/file_filter.py @@ -2,6 +2,7 @@ import fnmatch import re from pr_agent.config_loader import get_settings +from pr_agent.log import get_logger def filter_ignored(files, platform = 'github'): @@ -17,7 +18,17 @@ def filter_ignored(files, platform = 'github'): glob_setting = get_settings().ignore.glob if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py glob_setting = glob_setting.strip('[]').split(",") - patterns += [fnmatch.translate(glob) for glob in glob_setting] + patterns += translate_globs_to_regexes(glob_setting) + + code_generators = get_settings().config.get('ignore_language_framework', []) + if isinstance(code_generators, str): + get_logger().warning("'ignore_language_framework' should be a list. Skipping language framework filtering.") + code_generators = [] + for cg in code_generators: + glob_patterns = get_settings().generated_code.get(cg, []) + if isinstance(glob_patterns, str): + glob_patterns = [glob_patterns] + patterns += translate_globs_to_regexes(glob_patterns) # compile all valid patterns compiled_patterns = [] @@ -66,3 +77,11 @@ def filter_ignored(files, platform = 'github'): print(f"Could not filter file list: {e}") return files + +def translate_globs_to_regexes(globs: list): + regexes = [] + for pattern in globs: + regexes.append(fnmatch.translate(pattern)) + if pattern.startswith("**/"): # cover root-level files + regexes.append(fnmatch.translate(pattern[3:])) + return regexes diff --git a/pr_agent/config_loader.py b/pr_agent/config_loader.py index f525d893..f19f1067 100644 --- a/pr_agent/config_loader.py +++ b/pr_agent/config_loader.py @@ -14,6 +14,7 @@ global_settings = Dynaconf( settings_files=[join(current_dir, f) for f in [ "settings/configuration.toml", "settings/ignore.toml", + "settings/generated_code_ignore.toml", "settings/language_extensions.toml", "settings/pr_reviewer_prompts.toml", "settings/pr_questions_prompts.toml", diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index e3648fcc..8c9de39c 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -56,6 +56,7 @@ ignore_pr_source_branches = [] # a list of regular expressions of source branche ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created ignore_repositories = [] # a list of regular expressions of repository full names (e.g. "org/repo") to ignore from PR agent processing +ignore_language_framework = [] # a list of code-generation languages or frameworks (e.g. 'protobuf', 'go_gen') whose auto-generated source files will be excluded from analysis # is_auto_command = false # will be auto-set to true if the command is triggered by an automation enable_ai_metadata = false # will enable adding ai metadata diff --git a/pr_agent/settings/generated_code_ignore.toml b/pr_agent/settings/generated_code_ignore.toml new file mode 100644 index 00000000..52f3bfff --- /dev/null +++ b/pr_agent/settings/generated_code_ignore.toml @@ -0,0 +1,42 @@ +[generated_code] + +# Protocol Buffers +protobuf = [ + "**/*.pb.go", + "**/*.pb.cc", + "**/*_pb2.py", + "**/*.pb.swift", + "**/*.pb.rb", + "**/*.pb.php", + "**/*.pb.h" +] + +# OpenAPI / Swagger stubs +openapi = [ + "**/__generated__/**", + "**/openapi_client/**", + "**/openapi_server/**" +] +swagger = [ + "**/swagger.json", + "**/swagger.yaml" +] + +# GraphQL codegen +graphql = [ + "**/*.graphql.ts", + "**/*.generated.ts", + "**/*.graphql.js" +] + +# RPC / gRPC Generators +grpc_python = ["**/*_grpc.py"] +grpc_java = ["**/*Grpc.java"] +grpc_csharp = ["**/*Grpc.cs"] +grpc_typescript = ["**/*_grpc.ts", "**/*_grpc.js"] + +# Go code generators +go_gen = [ + "**/*_gen.go", + "**/*generated.go" +] diff --git a/tests/unittest/test_file_filter.py b/tests/unittest/test_file_filter.py index b35e351f..e461618c 100644 --- a/tests/unittest/test_file_filter.py +++ b/tests/unittest/test_file_filter.py @@ -80,3 +80,53 @@ class TestIgnoreFilter: filtered_files = filter_ignored(files) assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}." + + def test_language_framework_ignores(self, monkeypatch): + """ + Test files are ignored based on language/framework mapping (e.g., protobuf). + """ + monkeypatch.setattr(global_settings.config, 'ignore_language_framework', ['protobuf', 'go_gen']) + + files = [ + type('', (object,), {'filename': 'main.go'})(), + type('', (object,), {'filename': 'dir1/service.pb.go'})(), + type('', (object,), {'filename': 'dir1/dir/data_pb2.py'})(), + type('', (object,), {'filename': 'file.py'})(), + type('', (object,), {'filename': 'dir2/file_gen.go'})(), + type('', (object,), {'filename': 'file.generated.go'})() + ] + expected = [ + files[0], + files[3] + ] + + filtered = filter_ignored(files) + assert filtered == expected, ( + f"Expected {[f.filename for f in expected]}, " + f"but got {[f.filename for f in filtered]}" + ) + + def test_skip_invalid_ignore_language_framework(self, monkeypatch): + """ + Test skipping of generated code filtering when ignore_language_framework is not a list + """ + monkeypatch.setattr(global_settings.config, 'ignore_language_framework', 'protobuf') + + files = [ + type('', (object,), {'filename': 'main.go'})(), + type('', (object,), {'filename': 'file.py'})(), + type('', (object,), {'filename': 'dir1/service.pb.go'})(), + type('', (object,), {'filename': 'file_pb2.py'})() + ] + expected = [ + files[0], + files[1], + files[2], + files[3] + ] + + filtered = filter_ignored(files) + assert filtered == expected, ( + f"Expected {[f.filename for f in expected]}, " + f"but got {[f.filename for f in filtered]}" + )