Skip to content

Commit f4be5e8

Browse files
committed
eclass: add ShadowedEclassPhase for multiple-eclass defined phases
Detect the basic case of an ignored eclass phase where two eclasses are inherited, both defining the same phase, and the ebuild does not define a custom implementation of that phase at all. This means one of the exported phases from the eclasses is being ignored. Ignore some eclasses with a blacklist where they are known to vary their API by EAPI or eclass variables, at least for now, because the eclass cache we have accessible here isn't keyed by EAPI or the context of the sourcing ebuild. Bug: https://bugs.gentoo.org/516014 Bug: https://bugs.gentoo.org/795006 Closes: #377 Signed-off-by: Sam James <sam@gentoo.org>
1 parent 4d2ad0d commit f4be5e8

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

src/pkgcheck/checks/eclass.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,28 @@ def desc(self):
129129
return f"line {self.lineno}: redundant eclass inherit {self.line!r}, provided by {self.provider!r}"
130130

131131

132+
class ShadowedEclassPhase(results.VersionResult, results.Style):
133+
"""Ebuild does not define a phase when inheriting multiple eclasses
134+
exporting that phase.
135+
136+
When inheriting multiple eclasses exporting the same phase, a custom
137+
phase must usually be defined in the ebuild to call the phase exported
138+
from each eclass.
139+
140+
Exceptions exist where the functionality isn't needed, but it should
141+
be a deliberate choice, not an accidental omission.
142+
"""
143+
144+
def __init__(self, phase, providers, **kwargs):
145+
super().__init__(**kwargs)
146+
self.phase = phase
147+
self.providers = tuple(providers)
148+
149+
@property
150+
def desc(self):
151+
return f"missing custom phase for {self.phase!r}, provided by eclasses: {', '.join(self.providers)}"
152+
153+
132154
class EclassUsageCheck(Check):
133155
"""Scan packages for various eclass-related issues."""
134156

@@ -142,6 +164,7 @@ class EclassUsageCheck(Check):
142164
EclassUserVariableUsage,
143165
MisplacedEclassVar,
144166
ProvidedEclassInherit,
167+
ShadowedEclassPhase,
145168
}
146169
)
147170
required_addons = (addons.eclass.EclassAddon,)
@@ -254,6 +277,59 @@ def check_provided_eclasses(self, pkg, inherits: list[tuple[list[str], int]]):
254277
for provided, (eclass, lineno) in provided_eclasses.items():
255278
yield ProvidedEclassInherit(eclass, pkg=pkg, line=provided, lineno=lineno)
256279

280+
def check_exported_eclass_phase(
281+
self, pkg: bash.ParseTree, inherits: list[tuple[list[str], int]]
282+
):
283+
"""Check for eclasses exporting the same phase where the ebuild does not
284+
call such phases manually."""
285+
latest_eapi = EAPI.known_eapis[sorted(EAPI.known_eapis)[-1]]
286+
# all known build phases, e.g. src_configure
287+
known_phases = list(latest_eapi.phases_rev)
288+
exported_phases = {phase: [] for phase in known_phases}
289+
290+
# Create a dict of known phases => eclasses exporting them:
291+
# we're interested in the cases where the RHS list has > 1 element.
292+
for eclasses, _ in inherits:
293+
for eclass in eclasses:
294+
for func in self.eclass_cache[eclass].functions:
295+
phase = func.name.removeprefix(f"{eclass}_")
296+
if phase in known_phases:
297+
exported_phases[phase].append(eclass)
298+
299+
if not exported_phases.keys():
300+
return
301+
302+
defined_phases = []
303+
for node in bash.func_query.captures(pkg.tree.root_node).get("func", ()):
304+
func_name = pkg.node_str(node.child_by_field_name("name"))
305+
if func_name in known_phases:
306+
defined_phases.append(func_name)
307+
308+
# XXX: Some eclasses vary their API based on the EAPI, usually to
309+
# 'unexport' a phase. self.eclass_cache is generated once per eclass,
310+
# not (eclass, EAPI), so it can't handle this. Ditto phases which are only
311+
# exported if a variable is set. Blacklist such eclasses here as it
312+
# doesn't happen often.
313+
#
314+
# We could maybe make this more finely-grained for phases we know
315+
# are conditionally exported if this list is impacting coverage
316+
# severely.
317+
blacklisted_eclasses = ["pypi", "vala", "xdg"]
318+
exported_phases = {
319+
phase: set(eclass) - set(blacklisted_eclasses)
320+
for phase, eclass in exported_phases.items()
321+
}
322+
323+
# Strip out phases we already define (even if inside of those, we don't
324+
# actually call exported phases from all eclasses inherited). Assume that
325+
# a custom phase in the ebuild is intentionally omitting them.
326+
missing_custom_phases = set(
327+
phase for phase, eclass in exported_phases.items() if len(eclass) > 1
328+
) - set(defined_phases)
329+
330+
for missing in missing_custom_phases:
331+
yield ShadowedEclassPhase(missing, sorted(exported_phases[missing]), pkg=pkg)
332+
257333
def feed(self, pkg):
258334
if pkg.inherit:
259335
inherited: set[str] = set()
@@ -283,6 +359,7 @@ def feed(self, pkg):
283359
# verify @DEPRECATED variables or functions
284360
yield from self.check_deprecated_variables(pkg, inherits)
285361
yield from self.check_deprecated_functions(pkg, inherits)
362+
yield from self.check_exported_eclass_phase(pkg, inherits)
286363

287364
for eclass in pkg.inherit.intersection(self.deprecated_eclasses):
288365
replacement = self.deprecated_eclasses[eclass]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"__class__": "ShadowedEclassPhase", "category": "EclassUsageCheck", "package": "ShadowedEclassPhase", "version": "0", "phase": "src_prepare", "providers": ["another-src_prepare", "export-funcs-before-inherit"]}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--- eclass/EclassUsageCheck/ShadowedEclassPhase/ShadowedEclassPhase-0.ebuild
2+
+++ fixed/EclassUsageCheck/ShadowedEclassPhase/ShadowedEclassPhase-0.ebuild
3+
@@ -4,3 +4,9 @@ DESCRIPTION="Ebuild"
4+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
5+
LICENSE="BSD"
6+
SLOT="0"
7+
+
8+
+src_prepare() {
9+
+ default
10+
+ another-src_prepare_src_prepare
11+
+ export-funcs-before-inherit_src_prepare
12+
+}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
EAPI=7
2+
inherit another-src_prepare export-funcs-before-inherit
3+
DESCRIPTION="Ebuild"
4+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
5+
LICENSE="BSD"
6+
SLOT="0"

0 commit comments

Comments
 (0)