From a58a1c2f16fc05fbeee45f1e35b0b5a3d0335cf9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 26 Jan 2026 23:38:21 +0100 Subject: [PATCH] Transfer comments from template when re-saving project.yml files --- src/serena/config/serena_config.py | 19 ++++++++++++--- src/serena/util/yaml.py | 39 ++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 7e085ec1..6adf9e8a 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -29,7 +29,7 @@ from serena.constants import ( SERENA_MANAGED_DIR_NAME, ) from serena.util.inspection import determine_programming_language_composition -from serena.util.yaml import load_yaml, save_yaml +from serena.util.yaml import YamlCommentNormalisation, load_yaml, save_yaml, transfer_missing_yaml_comments from solidlsp.ls_config import Language from ..analytics import RegisteredTokenCountEstimator @@ -174,6 +174,11 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi SERENA_DEFAULT_PROJECT_FILE = "project.yml" FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"} + YAML_COMMENT_NORMALISATION = YamlCommentNormalisation.LEADING + """ + the comment normalisation strategy to use when loading/saving project configuration files. + The template file must match this configuration (i.e. it must use leading comments if this is set to LEADING). + """ def _tostring_includes(self) -> list[str]: return ["project_name"] @@ -265,7 +270,9 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi return os.path.join(SERENA_MANAGED_DIR_NAME, cls.SERENA_DEFAULT_PROJECT_FILE) @classmethod - def _load_yaml(cls, yml_path: str) -> tuple[CommentedMap, bool]: + def _load_yaml( + cls, yml_path: str, comment_normalisation: YamlCommentNormalisation = YamlCommentNormalisation.NONE + ) -> tuple[CommentedMap, bool]: """ Load the project configuration as a CommentedMap, preserving comments and ensuring completeness of the configuration by applying default values for missing fields @@ -276,7 +283,7 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi full project configuration and `was_complete` indicates whether the loaded configuration was complete (i.e., did not require any default values to be applied) """ - data = load_yaml(yml_path) + data = load_yaml(yml_path, comment_normalisation=comment_normalisation) # apply defaults was_complete = True @@ -379,9 +386,13 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi log.info("Saving updated project configuration to %s", config_path) # load original commented map and update it with current values - config_with_comments, _ = self._load_yaml(config_path) + config_with_comments, _ = self._load_yaml(config_path, self.YAML_COMMENT_NORMALISATION) config_with_comments.update(self._to_yaml_dict()) + # transfer missing comments from the template file + template_config, _ = self._load_yaml(PROJECT_TEMPLATE_FILE, self.YAML_COMMENT_NORMALISATION) + transfer_missing_yaml_comments(template_config, config_with_comments, self.YAML_COMMENT_NORMALISATION) + save_yaml(config_path, config_with_comments) diff --git a/src/serena/util/yaml.py b/src/serena/util/yaml.py index e9679d02..22928200 100644 --- a/src/serena/util/yaml.py +++ b/src/serena/util/yaml.py @@ -31,8 +31,8 @@ DOC_COMMENT_INDEX_POST = 0 DOC_COMMENT_INDEX_PRE = 1 # item comment indices: (post key, pre key, post value, pre value) -ITEM_COMMENT_INDEX_BEFORE = 1 # Must be a list at this index -ITEM_COMMENT_INDEX_AFTER = 2 # Not a list at this index +ITEM_COMMENT_INDEX_BEFORE = 1 # (pre-key; must be a list at this index) +ITEM_COMMENT_INDEX_AFTER = 2 # (post-value; typically no list at this index) def load_yaml(path: str, comment_normalisation: YamlCommentNormalisation = YamlCommentNormalisation.NONE) -> CommentedMap: @@ -81,3 +81,38 @@ def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = Tr os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding=SERENA_FILE_ENCODING) as f: yaml.dump(data, f) + + +def transfer_missing_yaml_comments_by_index(source: CommentedMap, target: CommentedMap, indices: list[int]) -> None: + """ + :param source: the source, from which to transfer missing comments + :param target: the target map, whose comments will be updated + :param indices: list of comment indices to transfer + """ + for key in target.keys(): + if key in source: + source_comment = source.ca.items.get(key) + if source_comment is None: + continue + target_comment = target.ca.items.get(key, [None, None, None, None]) + target.ca.items[key] = target_comment + for index in indices: + if target_comment[index] is None: + target_comment[index] = source_comment[index] + + +def transfer_missing_yaml_comments(source: CommentedMap, target: CommentedMap, comment_normalisation: YamlCommentNormalisation) -> None: + """ + Transfers missing comments from source to target YAML. + + :param source: the source, from which to transfer missing comments + :param target: the target map, whose comments will be updated. + :param comment_normalisation: the comment normalisation to assume; if NONE, no comments are transferred + """ + match comment_normalisation: + case YamlCommentNormalisation.NONE: + pass + case YamlCommentNormalisation.LEADING: + transfer_missing_yaml_comments_by_index(source, target, [ITEM_COMMENT_INDEX_BEFORE]) + case _: + raise ValueError(f"Unhandled comment normalisation: {comment_normalisation}")