commit:     d1e4aef5532f1d407169e7f53d6816ca7ba2da82
Author:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
AuthorDate: Thu Mar  2 20:20:50 2023 +0000
Commit:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Fri Mar  3 05:49:43 2023 +0000
URL:        
https://gitweb.gentoo.org/proj/pkgcore/pkgcheck.git/commit/?id=d1e4aef5

EbuildReservedCheck: check for semi-reserved names

Resolves: https://github.com/pkgcore/pkgcheck/issues/536
Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>

 src/pkgcheck/bash/__init__.py                      |  5 +--
 src/pkgcheck/checks/reserved.py                    | 48 +++++++++++++++++++---
 .../EbuildSemiReservedName/expected.json           |  5 +++
 .../EbuildSemiReservedName-0.ebuild                | 13 ++++++
 4 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/src/pkgcheck/bash/__init__.py b/src/pkgcheck/bash/__init__.py
index bff1a94d..70040981 100644
--- a/src/pkgcheck/bash/__init__.py
+++ b/src/pkgcheck/bash/__init__.py
@@ -1,6 +1,5 @@
 """bash parsing support"""
 
-from functools import partial
 import os
 
 from snakeoil.osutils import pjoin
@@ -100,7 +99,7 @@ except ImportError:  # pragma: no cover
 
 if syslib is not None or os.path.exists(lib):
     lang = Language(syslib or lib, "bash")
-    query = partial(lang.query)
+    query = lang.query
     parser = Parser()
     parser.set_language(lang)
 
@@ -114,7 +113,7 @@ if syslib is not None or os.path.exists(lib):
 class ParseTree:
     """Bash parse tree object and support."""
 
-    def __init__(self, data, **kwargs):
+    def __init__(self, data: bytes, **kwargs):
         super().__init__(**kwargs)
         self.data = data
         self.tree = parser.parse(data)

diff --git a/src/pkgcheck/checks/reserved.py b/src/pkgcheck/checks/reserved.py
index a67d1683..f99a9050 100644
--- a/src/pkgcheck/checks/reserved.py
+++ b/src/pkgcheck/checks/reserved.py
@@ -1,4 +1,5 @@
 import re
+import string
 
 from pkgcore.ebuild.eapi import EAPI
 
@@ -22,7 +23,7 @@ class _ReservedNameCheck(Check):
     """Approved good exceptions to using of variables."""
     variables_usage_whitelist = {"EBUILD_PHASE", "EBUILD_PHASE_FUNC"}
 
-    def _check(self, used_type: str, used_names):
+    def _check(self, used_type: str, used_names: dict[str, tuple[int, int]]):
         for used_name, (lineno, _) in used_names.items():
             if used_name in self.special_whitelist:
                 continue
@@ -36,7 +37,7 @@ class _ReservedNameCheck(Check):
             if self.reserved_ebuild_regex.match(test_name):
                 yield used_name, used_type, "ebuild", "substring", lineno + 1
 
-    def _feed(self, item):
+    def _feed(self, item: bash.ParseTree):
         yield from self._check(
             "function",
             {
@@ -82,7 +83,7 @@ class EclassReservedCheck(_ReservedNameCheck):
         super().__init__(*args)
         self.eclass_cache = eclass_addon.eclasses
 
-    def feed(self, eclass):
+    def feed(self, eclass: sources._ParsedEclass):
         for *args, _ in self._feed(eclass):
             yield EclassReservedName(*args, eclass=eclass.name)
 
@@ -101,11 +102,34 @@ class EbuildReservedName(results.LineResult, 
results.Warning):
         return f'line {self.lineno}: {self.used_type} name "{self.line}" is 
disallowed because "{self.reserved_word}" is a reserved {self.reserved_type}'
 
 
+class EbuildSemiReservedName(results.LineResult, results.Warning):
+    """Ebuild uses semi-reserved variable or function name.
+
+    Ebuild is using in global scope semi-reserved variable or function names,
+    which is likely to clash with future EAPIs. Currently it include
+    single-letter uppercase variables, and ``[A-Z]DEPEND`` variables.
+    """
+
+    def __init__(self, used_type: str, **kwargs):
+        super().__init__(**kwargs)
+        self.used_type = used_type
+
+    @property
+    def desc(self):
+        return f'line {self.lineno}: uses semi-reserved {self.used_type} name 
"{self.line}", likely to clash with future EAPIs'
+
+
 class EbuildReservedCheck(_ReservedNameCheck):
     """Scan ebuilds for reserved function or variable names."""
 
     _source = sources.EbuildParseRepoSource
-    known_results = frozenset([EbuildReservedName])
+    known_results = frozenset({EbuildReservedName, EbuildSemiReservedName})
+
+    global_reserved = (
+        frozenset(string.ascii_uppercase)
+        .union(c + "DEPEND" for c in string.ascii_uppercase)
+        .difference(("CDEPEND",))
+    )
 
     def __init__(self, options, **kwargs):
         super().__init__(options, **kwargs)
@@ -116,7 +140,7 @@ class EbuildReservedCheck(_ReservedNameCheck):
             for eapi_name, eapi in EAPI.known_eapis.items()
         }
 
-    def feed(self, pkg):
+    def feed(self, pkg: sources._ParsedPkg):
         for used_name, *args, lineno in self._feed(pkg):
             yield EbuildReservedName(*args, lineno=lineno, line=used_name, 
pkg=pkg)
 
@@ -127,3 +151,17 @@ class EbuildReservedCheck(_ReservedNameCheck):
                 yield EbuildReservedName(
                     "function", used_name, "phase hook", lineno=lineno + 1, 
line=used_name, pkg=pkg
                 )
+
+        current_global_reserved = self.global_reserved.difference(
+            pkg.eapi.eclass_keys, pkg.eapi.dep_keys
+        )
+        for node in pkg.global_query(bash.var_assign_query):
+            used_name = pkg.node_str(node.child_by_field_name("name"))
+            if used_name in current_global_reserved:
+                lineno, _ = node.start_point
+                yield EbuildSemiReservedName(
+                    "variable",
+                    lineno=lineno + 1,
+                    line=used_name,
+                    pkg=pkg,
+                )

diff --git 
a/testdata/data/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/expected.json
 
b/testdata/data/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/expected.json
new file mode 100644
index 00000000..c5916b53
--- /dev/null
+++ 
b/testdata/data/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/expected.json
@@ -0,0 +1,5 @@
+{"__class__": "EbuildSemiReservedName", "category": "DependencyCheck", 
"package": "MisplacedWeakBlocker", "version": "6", "line": "BDEPEND", "lineno": 
8, "used_type": "variable"}
+{"__class__": "EbuildSemiReservedName", "category": "DependencyCheck", 
"package": "MisplacedWeakBlocker", "version": "6", "line": "IDEPEND", "lineno": 
12, "used_type": "variable"}
+{"__class__": "EbuildSemiReservedName", "category": "DependencyCheck", 
"package": "MisplacedWeakBlocker", "version": "7", "line": "IDEPEND", "lineno": 
12, "used_type": "variable"}
+{"__class__": "EbuildSemiReservedName", "category": "EbuildReservedCheck", 
"package": "EbuildSemiReservedName", "version": "0", "line": "B", "lineno": 9, 
"used_type": "variable"}
+{"__class__": "EbuildSemiReservedName", "category": "EbuildReservedCheck", 
"package": "EbuildSemiReservedName", "version": "0", "line": "TDEPEND", 
"lineno": 13, "used_type": "variable"}

diff --git 
a/testdata/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/EbuildSemiReservedName-0.ebuild
 
b/testdata/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/EbuildSemiReservedName-0.ebuild
new file mode 100644
index 00000000..846fbb03
--- /dev/null
+++ 
b/testdata/repos/standalone/EbuildReservedCheck/EbuildSemiReservedName/EbuildSemiReservedName-0.ebuild
@@ -0,0 +1,13 @@
+EAPI=8
+
+DESCRIPTION="Ebuild with semi-reserved names"
+HOMEPAGE="https://github.com/pkgcore/pkgcheck";
+SLOT="0"
+LICENSE="BSD"
+
+S=${WORKDIR}  # ok
+B=${WORKDIR}  # fail
+BDEPEND="app-arch/unzip"  # ok
+CDEPEND="app-arch/unzip"  # ok
+RDEPEND="${CDEPEND}"  # ok
+TDEPEND="app-arch/unzip"  # fail

Reply via email to