libpfu_fixers/python/
deps.rs

1//! Python dependencies checks.
2
3use anyhow::Result;
4use async_trait::async_trait;
5use libabbs::apml::{lst, value::array::StringArray};
6use libpfu::{
7	Linter, Session, declare_lint, declare_linter,
8	message::{LintMessage, Snippet},
9	walk_defines,
10};
11use log::debug;
12
13use crate::python::depsolver;
14
15declare_linter! {
16	pub PYTHON_DEPS_LINTER,
17	PythonDepsLinter,
18	[
19		"python-suggested-dep",
20	]
21}
22
23declare_lint! {
24	pub PYTHON_SUGGEST_DEP_LINT,
25	"python-suggested-dep",
26	Note,
27	"some dependencies may be missed"
28}
29
30#[async_trait]
31impl Linter for PythonDepsLinter {
32	async fn apply(&self, sess: &Session) -> Result<()> {
33		if sess.offline {
34			return Ok(());
35		}
36		let mut py_deps = depsolver::collect_deps(sess).await?;
37		if py_deps.is_empty() {
38			debug!(
39				"{:?} does not have any Python dependencies found",
40				sess.package
41			);
42			return Ok(());
43		} else {
44			debug!(
45				"Collected Python dependencies of {:?}: {:?}",
46				sess.package, py_deps
47			);
48		}
49
50		for mut apml in walk_defines(sess) {
51			debug!("Checking Python dependencies for {:?}", apml);
52			let abtype = apml.with_upgraded(|apml| {
53				apml.ctx()
54					.map(|ctx| ctx.get("ABTYPE").map(|val| val.as_string()))
55			})?;
56			if let Some(abtype) = abtype {
57				if abtype != "pep517" && abtype != "python" {
58					debug!(
59						"Explicit ABTYPE '{abtype}' is not Python, skipping PEP-517 lints"
60					);
61					continue;
62				}
63			}
64
65			let [pkgdep, builddep] = ["PKGDEP", "BUILDDEP"].map(|var| {
66				apml.with_upgraded(|apml| {
67					apml.ctx().map(|ctx| {
68						ctx.get(var)
69							.map(|val| val.as_string())
70							.unwrap_or_default()
71					})
72				})
73				.map(StringArray::from)
74			});
75			let (mut pkgdep, mut builddep) = (pkgdep?, builddep?);
76			let mut pkgdep_dirty = false;
77
78			for dep in &mut py_deps {
79				if let Some(prov_pkg) =
80					depsolver::find_system_package(dep, &pkgdep, &builddep)
81						.await?
82				{
83					if pkgdep.contains(&prov_pkg)
84						|| (dep.build_dep && builddep.contains(&prov_pkg))
85					{
86						continue;
87					}
88
89					apml.with_upgraded(|apml| {
90						LintMessage::new(PYTHON_SUGGEST_DEP_LINT)
91							.snippet(Snippet::new_variable(
92								sess,
93								apml,
94								if dep.build_dep {
95									"BUILDDEP"
96								} else {
97									"PKGDEP"
98								},
99							))
100							.note(format!(
101								"package '{prov_pkg}' provides {} dependency '{}'",
102								if dep.build_dep { "build" } else { "runtime" },
103								dep.name,
104							))
105							.note(format!(
106								"requirement '{}' found in {}",
107								dep.raw_req, dep.origin,
108							))
109							.emit(sess);
110
111						if !sess.dry {
112							if !dep.build_dep {
113								pkgdep.push(prov_pkg.clone());
114							} else {
115								builddep.push(prov_pkg.clone());
116							}
117							pkgdep_dirty = true;
118						}
119					});
120				}
121			}
122			if pkgdep_dirty {
123				apml.with_upgraded(|apml| {
124					apml.with_editor(|apml| {
125						apml.replace_var_lst(
126							"PKGDEP",
127							lst::VariableValue::String(pkgdep.print().into()),
128						);
129						apml.replace_var_lst(
130							"BUILDDEP",
131							lst::VariableValue::String(builddep.print().into()),
132						);
133					})
134				});
135			}
136		}
137		Ok(())
138	}
139}