Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/02-usage/040_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 7 additions & 0 deletions src/serena/resources/project.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: []

Expand Down
1 change: 1 addition & 0 deletions src/serena/resources/serena_config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion src/serena/util/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions test/serena/config/test_serena_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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."""
Expand Down
Loading