From 70ae961aba619c2cde4151772e31a079dde70226 Mon Sep 17 00:00:00 2001 From: Edmund Miller Date: Mon, 6 Jul 2026 22:51:31 -0500 Subject: [PATCH] feat(modules): flag legacy versions emits --- nf_core/modules/lint/main_nf.py | 18 ++++++++++++++++- .../rules/main_nf_legacy_versions_emit.yml | 17 ++++++++++++++++ tests/modules/lint/test_main_nf.py | 20 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 nf_core/modules/lint/rules/main_nf_legacy_versions_emit.yml diff --git a/nf_core/modules/lint/main_nf.py b/nf_core/modules/lint/main_nf.py index 5caa70cfcc..5d45bf9b22 100644 --- a/nf_core/modules/lint/main_nf.py +++ b/nf_core/modules/lint/main_nf.py @@ -15,6 +15,7 @@ log = logging.getLogger(__name__) MAIN_NF_VERSIONS_YML_TOPIC_RULE = Path(__file__).parent / "rules" / "main_nf_versions_yml_topic.yml" +MAIN_NF_LEGACY_VERSIONS_EMIT_RULE = Path(__file__).parent / "rules" / "main_nf_legacy_versions_emit.yml" def main_nf( @@ -323,7 +324,7 @@ def main_nf( module.passed.append( ("main_nf", "main_nf_version_emit", "Module emits each software version", module.main_nf) ) - elif "versions" in emits: + elif _has_legacy_versions_emit(lines_j, lines): module.warned.append( ( "main_nf", @@ -972,6 +973,21 @@ def _parse_output_topics(self, line: str) -> list[str]: return output +def _has_legacy_versions_emit(source: str, output_lines: list[str]) -> bool: + rule = astgrep.load_rule(MAIN_NF_LEGACY_VERSIONS_EMIT_RULE) + if astgrep.nextflow_available(): + from ast_grep_py import SgRoot + + root = SgRoot(source, "nextflow").root() + if root.find(kind="ERROR") is None: + config = {key: rule[key] for key in ("rule", "constraints", "utils") if key in rule} + return bool(root.find_all(config=config)) + + return any( + re.search(r"emit:\s*versions\b", line) and not re.search(r"topic:\s*versions\b", line) for line in output_lines + ) + + def _has_inline_versions_yml_topic_output(source: str, output_lines: list[str], command_lines: list[str]) -> bool: rule = astgrep.load_rule(MAIN_NF_VERSIONS_YML_TOPIC_RULE) if astgrep.nextflow_available(): diff --git a/nf_core/modules/lint/rules/main_nf_legacy_versions_emit.yml b/nf_core/modules/lint/rules/main_nf_legacy_versions_emit.yml new file mode 100644 index 0000000000..964ad2dbfa --- /dev/null +++ b/nf_core/modules/lint/rules/main_nf_legacy_versions_emit.yml @@ -0,0 +1,17 @@ +# ast-grep lint rule (https://ast-grep.github.io/guide/project/lint-rule.html) +id: main_nf_legacy_versions_emit +language: nextflow +severity: warning +message: "legacy `versions` output emit" +note: | + Legacy `emit: versions` outputs should migrate to version tuple outputs with + `topic: versions`. Template-backed modules that also set `topic: versions` + are intentionally allowed. +metadata: + fallback-regex: a^ +rule: + kind: output_declaration + all: + - regex: 'emit:\s*versions\b' + - not: + regex: 'topic:\s*versions\b' diff --git a/tests/modules/lint/test_main_nf.py b/tests/modules/lint/test_main_nf.py index 04a6e4e981..05aa91eeb3 100644 --- a/tests/modules/lint/test_main_nf.py +++ b/tests/modules/lint/test_main_nf.py @@ -8,6 +8,7 @@ from nf_core.components.nfcore_component import NFCoreComponent from nf_core.modules.lint.main_nf import ( _has_inline_versions_yml_topic_output, + _has_legacy_versions_emit, _parse_output_topics, check_container_link_line, check_nf_module_name, @@ -43,6 +44,25 @@ def test_inline_versions_yml_topic_output_matches_full_source_with_astgrep(): assert _has_inline_versions_yml_topic_output(source, [], []) +@pytest.mark.skipif(not astgrep.nextflow_available(), reason="tree-sitter-nextflow parser not available") +@pytest.mark.parametrize( + "output_line, expected", + [ + ('path "versions.yml", emit: versions', True), + ('path "versions.yml", emit: versions, topic: versions', False), + ], +) +def test_legacy_versions_emit_astgrep_detection(output_line, expected): + source = f""" +process TEST {{ + output: + {output_line} +}} +""" + + assert _has_legacy_versions_emit(source, []) is expected + + @pytest.mark.parametrize( "content,passed,warned,failed", [