Skip to content

Commit 44cc2e3

Browse files
committed
GlobalDeclareCheck: new check for declare -A without -g in global scope
Any use of declare is local scope unless a -g is passed. Missing -g can cause issues when the ebuild is sourced inside a function (as non-portage package managers may do). One example is pkgcore: when it processes ebuilds with declare -A without -g, it usually fails in configure phase (good outcome) or worse, it silently skips some files in install phase. The new check is added as a warning since Portage is not affected. Closes: #628 Signed-off-by: Sv. Lockal <lockalsash@gmail.com>
1 parent 5986de6 commit 44cc2e3

7 files changed

Lines changed: 108 additions & 0 deletions

File tree

NEWS.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pkgcheck 0.10.40 (unreleased)
1414
- StabilizationGroupsCheck: check for invalid and non-existant stabilization
1515
groups (Arthur Zamarin)
1616

17+
- GlobalDeclareCheck: detect ``declare -A`` without ``-g`` in global scope
18+
(Sv. Lockal, #628)
19+
1720

1821
**Packaging:**
1922

src/pkgcheck/bash/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def query(query_str: str):
4949
var_assign_query = query("(variable_assignment) @assign")
5050
var_expansion_query = query("(expansion) @exp")
5151
var_query = query("(variable_name) @var")
52+
decl_query = query("(declaration_command) @decl")
5253

5354

5455
class ParseTree:

src/pkgcheck/checks/codingstyle.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,3 +1667,45 @@ def feed(self, pkg: bash.ParseTree):
16671667
if new_index < index:
16681668
yield VariableOrderWrong(first_var, self.variable_order[index], pkg=pkg)
16691669
index = new_index
1670+
1671+
1672+
class GlobalDeclareWithoutG(results.LineResult, results.Warning):
1673+
"""Call to ``declare -A`` without ``-g`` in global scope.
1674+
1675+
Associative arrays created with ``declare -A`` in global scope
1676+
are implicitly local when the ebuild is sourced inside a function
1677+
(as non-portage package managers may do).
1678+
Use ``declare -gA`` to ensure the variable is always in global scope.
1679+
"""
1680+
1681+
@property
1682+
def desc(self):
1683+
return f"line {self.lineno}: call to 'declare -A' without '-g' in global scope: {self.line}"
1684+
1685+
1686+
class GlobalDeclareCheck(Check):
1687+
"""Scan ebuilds for ``declare -A`` calls without ``-g`` in global scope."""
1688+
1689+
_source = sources.EbuildParseRepoSource
1690+
known_results = frozenset({GlobalDeclareWithoutG})
1691+
1692+
def feed(self, pkg: bash.ParseTree):
1693+
for node in pkg.global_query(bash.decl_query):
1694+
name_node = node.children[0]
1695+
if pkg.node_str(name_node) != "declare":
1696+
continue
1697+
has_A = False
1698+
has_g = False
1699+
for child in node.children:
1700+
if child.type == "word":
1701+
flag = pkg.node_str(child)
1702+
if flag.startswith("-"):
1703+
if "A" in flag:
1704+
has_A = True
1705+
if "g" in flag:
1706+
has_g = True
1707+
if has_A and not has_g:
1708+
lineno, _ = node.start_point
1709+
yield GlobalDeclareWithoutG(
1710+
line=pkg.node_str(node), lineno=lineno + 1, pkg=pkg
1711+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"__class__": "GlobalDeclareWithoutG", "category": "GlobalDeclareCheck", "package": "GlobalDeclareWithoutG", "version": "0", "line": "declare -A ASSOC_ARRAY=(\n\t[a]=b\n\t[c]=d\n)", "lineno": 11}
2+
{"__class__": "GlobalDeclareWithoutG", "category": "GlobalDeclareCheck", "package": "GlobalDeclareWithoutG", "version": "0", "line": "declare -rA READONLY_ASSOC=(\n\t[e]=f\n)", "lineno": 16}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
diff -Naur standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild fixed/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild
2+
--- standalone/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild
3+
+++ fixed/GlobalDeclareCheck/GlobalDeclareWithoutG/GlobalDeclareWithoutG-0.ebuild
4+
@@ -5,11 +5,11 @@
5+
LICENSE="BSD"
6+
SLOT="0"
7+
8+
-declare -A ASSOC_ARRAY=(
9+
+declare -gA ASSOC_ARRAY=(
10+
[a]=b
11+
[c]=d
12+
)
13+
14+
-declare -rA READONLY_ASSOC=(
15+
+declare -grA READONLY_ASSOC=(
16+
[e]=f
17+
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2026 Gentoo Authors
2+
# Distributed under the terms of the GNU General Public License v2
3+
4+
EAPI=8
5+
6+
DESCRIPTION="Ebuild with declare without -g in global scope"
7+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
8+
LICENSE="BSD"
9+
SLOT="0"
10+
11+
declare -A ASSOC_ARRAY=(
12+
[a]=b
13+
[c]=d
14+
)
15+
16+
declare -rA READONLY_ASSOC=(
17+
[e]=f
18+
)
19+
20+
declare -gA GOOD_ASSOC=(
21+
[g]=h
22+
)
23+
24+
declare -g -A ALSO_GOOD=(
25+
[i]=j
26+
)
27+
28+
declare -Ag YET_ANOTHER_GOOD=(
29+
[k]=l
30+
)
31+
32+
declare -rAg GOOD_READONLY=(
33+
[m]=n
34+
)
35+
36+
declare -r READONLY_VAR="foo"
37+
38+
src_prepare() {
39+
declare -A LOCAL_VAR=(
40+
[o]=p
41+
)
42+
}

testdata/repos/standalone/profiles/categories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ EclassManualDepsCheck
2020
EclassUsageCheck
2121
EendMissingArgCheck
2222
EqualVersionsCheck
23+
GlobalDeclareCheck
2324
GlobalUseCheck
2425
GlobCheck
2526
HomepageCheck

0 commit comments

Comments
 (0)