Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
- fix incorrect unqoting of `val()` version numbers ([#4042](https://github.com/nf-core/tools/pull/4042))
- allow versions.yml in the version topics ([#4094](https://github.com/nf-core/tools/pull/4094))
- Ensure release linting happens on branch named main ([#4121](https://github.com/nf-core/tools/pull/4121))
- Add linting for meta and ext keys ([#4127](https://github.com/nf-core/tools/pull/4127))
- Add strict syntax linting to template with pre-commit ([#4128](https://github.com/nf-core/tools/pull/4128))

### Modules
Expand Down
54 changes: 52 additions & 2 deletions nf_core/modules/lint/main_nf.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,61 @@ def check_script_section(self, lines):
# check for prefix (only if module has a meta map as input)
if self.has_meta:
if re.search(r"\s*prefix\s*=\s*task.ext.prefix", script):
self.passed.append(("main_nf", "main_nf_meta_prefix", "'prefix' specified in script section", self.main_nf))
self.passed.append(
(
"main_nf",
"main_nf_meta_prefix",
"'prefix' specified in script section",
self.main_nf,
)
)
else:
self.failed.append(
("main_nf", "main_nf_meta_prefix", "'prefix' unspecified in script section", self.main_nf)
(
"main_nf",
"main_nf_meta_prefix",
"'prefix' unspecified in script section",
self.main_nf,
)
)

# Validate meta keys
permitted_meta_keys = {"id", "single_end"}
invalid_meta_keys = [
f"{prefix}{key}"
for prefix, key in re.findall(r"(meta\d*\.)(\w+)\b(?!\()", script)
if key not in permitted_meta_keys
]
if not invalid_meta_keys:
self.passed.append(("main_nf", "main_nf_meta_key", "All 'meta' keys are valid", self.main_nf))
else:
self.failed.append(
(
"main_nf",
"main_nf_meta_key",
f"Invalid 'meta' keys detected: {', '.join(invalid_meta_keys)}",
self.main_nf,
)
)

# Validate ext keys
permitted_ext_keys = {"ext.args", "ext.prefix", "ext.use_gpu"}
invalid_ext_keys = [
key
for key in re.findall(r"ext\.\w+", script)
if key not in permitted_ext_keys and not re.match(r"^ext\.args([2-9]|\d{2,})$", key)
]
if not invalid_ext_keys:
self.passed.append(("main_nf", "main_nf_ext_key", "All 'ext' keys are valid", self.main_nf))
else:
self.failed.append(
(
"main_nf",
"main_nf_ext_key",
f"Invalid 'ext' keys detected: {', '.join(invalid_ext_keys)}",
self.main_nf,
)
)


def check_when_section(self, lines):
Expand Down
1 change: 1 addition & 0 deletions tests/modules/lint/test_lint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(self):
self.failed = []

self.main_nf = "main_nf"
self.has_meta = False


class TestModulesLint(TestModules):
Expand Down
122 changes: 121 additions & 1 deletion tests/modules/lint/test_main_nf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import nf_core.modules.lint
from nf_core.components.nfcore_component import NFCoreComponent
from nf_core.modules.lint.main_nf import _parse_output_topics, check_container_link_line, check_process_labels
from nf_core.modules.lint.main_nf import (
_parse_output_topics,
check_container_link_line,
check_process_labels,
check_script_section,
)

from ...test_modules import TestModules
from .test_lint_utils import MockModuleLint
Expand Down Expand Up @@ -517,3 +522,118 @@ def test_get_topics_version_yml_path_no_parens(tmp_path):
assert any("wrong_versions_yml_emit" in str(f) for f in mock_lint_fail.failed), (
f"Expected wrong_versions_yml_emit in failed, got: {mock_lint_fail.failed}"
)


def test_validate_meta_keys():
"""Test validation of meta keys in script"""
mock_lint = MockModuleLint()

# Valid meta keys
check_script_section(
mock_lint,
[
"""
def prefix = "${meta.id}"
def se = meta.single_end
def id = meta.subMap(['id'])
"""
],
)
assert len(mock_lint.failed) == 0

# Invalid meta keys
mock_lint.passed, mock_lint.failed = [], []
check_script_section(
mock_lint,
[
"""
def sample = meta.sample
def strand = meta.strandedness
"""
],
)
assert len(mock_lint.failed) == 1
assert "meta.sample" in mock_lint.failed[0][2]
assert "meta.strandedness" in mock_lint.failed[0][2]

# meta2/meta3 with valid keys
mock_lint.passed, mock_lint.failed = [], []
check_script_section(
mock_lint,
[
"""
def id1 = meta.id
def id2 = meta2.id
def se = meta3.single_end
"""
],
)
assert len(mock_lint.failed) == 0

# Mix of valid and invalid
mock_lint.passed, mock_lint.failed = [], []
check_script_section(
mock_lint,
[
"""
def prefix = task.ext.prefix ?: "${meta.id}"
def sample = meta.sample
def single_end = meta.single_end
def custom = meta2.custom_field
"""
],
)
assert len(mock_lint.failed) == 1
assert "meta.sample" in mock_lint.failed[0][2]
assert "meta2.custom_field" in mock_lint.failed[0][2]


def test_validate_ext_keys():
"""Test validation of ext keys in script"""
mock_lint = MockModuleLint()

# Valid ext keys
check_script_section(
mock_lint,
[
"""
def args = task.ext.args ?: ''
def args2 = task.ext.args2 ?: ''
def args3 = task.ext.args3 ?: ''
def prefix = task.ext.prefix ?: "${meta.id}"
def use_gpu = task.ext.use_gpu ? '--gpu' : ''
"""
],
)
assert len(mock_lint.failed) == 0

# Invalid ext keys
mock_lint.passed, mock_lint.failed = [], []
check_script_section(
mock_lint,
[
"""
def args1 = task.ext.args1 ?: ''
def custom = task.ext.custom ?: ''
def suffix = task.ext.suffix ?: '.bam'
"""
],
)
assert len(mock_lint.failed) == 1
assert "ext.args1" in mock_lint.failed[0][2]
assert "ext.custom" in mock_lint.failed[0][2]
assert "ext.suffix" in mock_lint.failed[0][2]

# ext.argsN where N >= 2 should be valid
mock_lint.passed, mock_lint.failed = [], []
check_script_section(
mock_lint,
[
"""
def args2 = task.ext.args2 ?: ''
def args10 = task.ext.args10 ?: ''
def args99 = task.ext.args99 ?: ''
"""
],
)
assert len(mock_lint.failed) == 0
Loading