Skip to content

Commit 6c71cc7

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 6c71cc7

4 files changed

Lines changed: 89 additions & 0 deletions

File tree

src/pkgcheck/checks/eclass.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,25 @@ 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 be defined in the ebuild to call the phase exported from
138+
each eclass.
139+
"""
140+
141+
def __init__(self, phase, providers, **kwargs):
142+
super().__init__(**kwargs)
143+
self.phase = phase
144+
self.providers = tuple(providers)
145+
146+
@property
147+
def desc(self):
148+
return f"missing custom phase for {self.phase!r}, provided by {' '.join(self.providers)}"
149+
150+
132151
class EclassUsageCheck(Check):
133152
"""Scan packages for various eclass-related issues."""
134153

@@ -142,6 +161,7 @@ class EclassUsageCheck(Check):
142161
EclassUserVariableUsage,
143162
MisplacedEclassVar,
144163
ProvidedEclassInherit,
164+
ShadowedEclassPhase,
145165
}
146166
)
147167
required_addons = (addons.eclass.EclassAddon,)
@@ -254,6 +274,55 @@ def check_provided_eclasses(self, pkg, inherits: list[tuple[list[str], int]]):
254274
for provided, (eclass, lineno) in provided_eclasses.items():
255275
yield ProvidedEclassInherit(eclass, pkg=pkg, line=provided, lineno=lineno)
256276

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

287357
for eclass in pkg.inherit.intersection(self.deprecated_eclasses):
288358
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)