diff --git a/NEWS.rst b/NEWS.rst index be4780f1a..ca55aef17 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -14,6 +14,9 @@ pkgcheck 0.10.40 (unreleased) - StabilizationGroupsCheck: check for invalid and non-existant stabilization groups (Arthur Zamarin) +- GlobalDeclareCheck: detect ``declare -A`` without ``-g`` in global scope + (Sv. Lockal, #628) + **Packaging:** diff --git a/src/pkgcheck/bash/__init__.py b/src/pkgcheck/bash/__init__.py index 020cc4128..7ff23d0d9 100644 --- a/src/pkgcheck/bash/__init__.py +++ b/src/pkgcheck/bash/__init__.py @@ -49,6 +49,7 @@ def query(query_str: str): var_assign_query = query("(variable_assignment) @assign") var_expansion_query = query("(expansion) @exp") var_query = query("(variable_name) @var") +decl_query = query("(declaration_command) @decl") class ParseTree: diff --git a/src/pkgcheck/checks/codingstyle.py b/src/pkgcheck/checks/codingstyle.py index 7091c45c4..b2c5ebb1c 100644 --- a/src/pkgcheck/checks/codingstyle.py +++ b/src/pkgcheck/checks/codingstyle.py @@ -1667,3 +1667,45 @@ def feed(self, pkg: bash.ParseTree): if new_index < index: yield VariableOrderWrong(first_var, self.variable_order[index], pkg=pkg) index = new_index + + +class GlobalDeclareWithoutG(results.LineResult, results.Warning): + """Call to ``declare -A`` without ``-g`` in global scope. + + Associative arrays created with ``declare -A`` in global scope + are implicitly local when the ebuild is sourced inside a function + (as non-portage package managers may do). + Use ``declare -gA`` to ensure the variable is always in global scope. + """ + + @property + def desc(self): + return f"line {self.lineno}: call to 'declare -A' without '-g' in global scope: {self.line}" + + +class GlobalDeclareCheck(Check): + """Scan ebuilds for ``declare -A`` calls without ``-g`` in global scope.""" + + _source = sources.EbuildParseRepoSource + known_results = frozenset({GlobalDeclareWithoutG}) + + def feed(self, pkg: bash.ParseTree): + for node in pkg.global_query(bash.decl_query): + name_node = node.children[0] + if pkg.node_str(name_node) != "declare": + continue + has_A = False + has_g = False + for child in node.children: + if child.type == "word": + flag = pkg.node_str(child) + if flag.startswith("-"): + if "A" in flag: + has_A = True + if "g" in flag: + has_g = True + if has_A and not has_g: + lineno, _ = node.start_point + yield GlobalDeclareWithoutG( + line=pkg.node_str(node), lineno=lineno + 1, pkg=pkg + ) diff --git a/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/expected.json b/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/expected.json new file mode 100644 index 000000000..a253ab766 --- /dev/null +++ b/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/expected.json @@ -0,0 +1,2 @@ +{"__class__": "GlobalDeclareWithoutG", "category": "GlobalDeclareCheck", "package": "GlobalDeclareWithoutG", "version": "0", "line": "declare -A ASSOC_ARRAY=(\n\t[a]=b\n\t[c]=d\n)", "lineno": 11} +{"__class__": "GlobalDeclareWithoutG", "category": "GlobalDeclareCheck", "package": "GlobalDeclareWithoutG", "version": "0", "line": "declare -rA READONLY_ASSOC=(\n\t[e]=f\n)", "lineno": 16} diff --git a/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/fix.patch b/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/fix.patch new file mode 100644 index 000000000..9899e4d82 --- /dev/null +++ b/testdata/data/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/fix.patch @@ -0,0 +1,17 @@ +diff -Naur standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild fixed/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild +--- standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild ++++ fixed/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild +@@ -5,11 +5,11 @@ + LICENSE="BSD" + SLOT="0" + +-declare -A ASSOC_ARRAY=( ++declare -gA ASSOC_ARRAY=( + [a]=b + [c]=d + ) + +-declare -rA READONLY_ASSOC=( ++declare -grA READONLY_ASSOC=( + [e]=f + ) diff --git a/testdata/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild b/testdata/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild new file mode 100644 index 000000000..ba44cd65f --- /dev/null +++ b/testdata/repos/standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild @@ -0,0 +1,42 @@ +# Copyright 2026 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +DESCRIPTION="Ebuild with declare without -g in global scope" +HOMEPAGE="https://github.com/pkgcore/pkgcheck" +LICENSE="BSD" +SLOT="0" + +declare -A ASSOC_ARRAY=( + [a]=b + [c]=d +) + +declare -rA READONLY_ASSOC=( + [e]=f +) + +declare -gA GOOD_ASSOC=( + [g]=h +) + +declare -g -A ALSO_GOOD=( + [i]=j +) + +declare -Ag YET_ANOTHER_GOOD=( + [k]=l +) + +declare -rAg GOOD_READONLY=( + [m]=n +) + +declare -r READONLY_VAR="foo" + +src_prepare() { + declare -A LOCAL_VAR=( + [o]=p + ) +} diff --git a/testdata/repos/standalone/profiles/categories b/testdata/repos/standalone/profiles/categories index 3466f4ccf..c5c93322c 100644 --- a/testdata/repos/standalone/profiles/categories +++ b/testdata/repos/standalone/profiles/categories @@ -20,6 +20,7 @@ EclassManualDepsCheck EclassUsageCheck EendMissingArgCheck EqualVersionsCheck +GlobalDeclareCheck GlobalUseCheck GlobCheck HomepageCheck