diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index 8fa10eca19..f7d9f6568b 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -69,6 +69,18 @@ You can specify local overrides for the settings in a `project.local.yml` file i (which, by default, is ignored by git). Any keys defined therein will override the respective key in `project.yml`. +For large C# / Roslyn repositories, `ignored_paths` is often the easiest way to keep nested examples, +worktrees, and build output directories out of project discovery. Use normal gitignore-style patterns, +but **quote any entry that starts with `*`**, for example: + +```yaml +ignored_paths: + - "examples/**" + - ".worktrees/**" + - "**/bin/**" + - "**/obj/**" +``` + (additional-workspace-folders)= #### Additional Workspace Folders (Cross-Package References) diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index a4dddb377c..dd3a4d7be6 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -71,6 +71,13 @@ additional_workspace_folders: [] # list of additional paths to ignore in this project. # Same syntax as gitignore, so you can use * and **. +# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases. +# Example: +# ignored_paths: +# - "examples/**" +# - ".worktrees/**" +# - "**/bin/**" +# - "**/obj/**" # Note: global ignored_paths from serena_config.yml are also applied additively. ignored_paths: [] diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index fb5c1d4096..a5d81d0d9e 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -103,6 +103,7 @@ ls_specific_settings: {} # list of paths to ignore across all projects. # Same syntax as gitignore, so you can use * and **. # These patterns are merged additively with each project's own ignored_paths. +# Quote patterns that start with `*`, e.g. `"**/bin/**"`. ignored_paths: [] # list of regex patterns which, when matched, mark a memory entry as read‑only. diff --git a/src/serena/util/yaml.py b/src/serena/util/yaml.py index ca63eee7b7..b1261e01eb 100644 --- a/src/serena/util/yaml.py +++ b/src/serena/util/yaml.py @@ -68,7 +68,17 @@ def load_yaml(path: str, comment_normalisation: YamlCommentNormalisation = YamlC """ with open(path, encoding=SERENA_FILE_ENCODING) as f: yaml = _create_yaml(preserve_comments=True) - commented_map: CommentedMap | None = yaml.load(f) + try: + commented_map: CommentedMap | None = yaml.load(f) + except Exception as e: + msg = str(e) + if "undefined alias" in msg: + raise ValueError( + f"Invalid YAML in {path}: values that start with `*` must be quoted. " + "This often happens in `ignored_paths` when using gitignore-style globs like " + '`"**/bin/**"` or `"**/obj/**"`.' + ) from e + raise if commented_map is None: # ruamel returns None for empty documents, but we want an empty CommentedMap commented_map = CommentedMap() normalise_yaml_comments(commented_map, comment_normalisation) diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 2b0a4d5266..7043ed9b60 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -499,6 +499,25 @@ def test_configured_path_takes_precedence_when_exists(self): assert project.path_to_serena_data_folder() == str(custom_serena) +class TestProjectConfigYamlValidation: + def test_ignored_paths_globs_starting_with_star_require_quotes(self): + project_dir = Path(tempfile.mkdtemp()) + try: + serena_dir = project_dir / SERENA_MANAGED_DIR_NAME + serena_dir.mkdir(parents=True) + (serena_dir / "project.yml").write_text('project_name: "demo"\nlanguages: ["csharp"]\nignored_paths:\n- **/bin/**\n') + + with pytest.raises(ValueError) as exc_info: + ProjectConfig.load(project_dir, create_default_serena_config()) + + msg = str(exc_info.value) + assert "values that start with `*` must be quoted" in msg + assert "ignored_paths" in msg + assert '"**/bin/**"' in msg + finally: + shutil.rmtree(project_dir) + + class TestSerenaConfigFromConfigFileRobustness: """Tests that ``SerenaConfig.from_config_file`` does not abort the whole loader when a single registered project has a broken ``project.yml``. @@ -568,6 +587,30 @@ def test_malformed_project_is_skipped_with_warning(self, caplog, monkeypatch): f"Expected a warning naming {bad_project.resolve()}, got: {caplog.messages}" ) + def test_alias_like_ignored_path_error_is_logged_with_hint(self, caplog, monkeypatch): + good_project = self._make_project_dir( + "good_project", + 'project_name: "good_project"\nlanguages: ["python"]\n', + ) + bad_project = self._make_project_dir( + "bad_project", + 'project_name: "bad_project"\nlanguages: ["csharp"]\nignored_paths:\n- **/bin/**\n', + ) + self._write_master_config([good_project, bad_project]) + + monkeypatch.setattr( + SerenaConfig, + "_determine_config_file_path", + classmethod(lambda cls: str(self.master_config_path)), + ) + + with caplog.at_level(logging.ERROR): + config = SerenaConfig.from_config_file(generate_if_missing=False) + + registered_roots = {Path(p.project_root).resolve() for p in config.projects} + assert registered_roots == {good_project.resolve()} + assert any("must be quoted" in msg and str(bad_project.resolve()) in msg for msg in caplog.messages), caplog.messages + class TestMemoriesManagerCustomPath: """Tests for MemoriesManager with a custom serena data folder."""