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				&& abtype != "pep517" && abtype != "python" {
58					debug!(
59						"Explicit ABTYPE '{abtype}' is not Python, skipping PEP-517 lints"
60					);
61					continue;
62				}
63
64			let [pkgdep, builddep] = ["PKGDEP", "BUILDDEP"].map(|var| {
65				apml.with_upgraded(|apml| {
66					apml.ctx().map(|ctx| {
67						ctx.get(var)
68							.map(|val| val.as_string())
69							.unwrap_or_default()
70					})
71				})
72				.map(StringArray::from)
73			});
74			let (mut pkgdep, mut builddep) = (pkgdep?, builddep?);
75			let mut pkgdep_dirty = false;
76
77			for dep in &mut py_deps {
78				if let Some(prov_pkg) =
79					depsolver::find_system_package(dep, &pkgdep, &builddep)
80						.await?
81				{
82					if pkgdep.contains(&prov_pkg)
83						|| (dep.build_dep && builddep.contains(&prov_pkg))
84					{
85						continue;
86					}
87
88					apml.with_upgraded(|apml| {
89						LintMessage::new(PYTHON_SUGGEST_DEP_LINT)
90							.snippet(Snippet::new_variable(
91								sess,
92								apml,
93								if dep.build_dep {
94									"BUILDDEP"
95								} else {
96									"PKGDEP"
97								},
98							))
99							.note(format!(
100								"package '{prov_pkg}' provides {} dependency '{}'",
101								if dep.build_dep { "build" } else { "runtime" },
102								dep.name,
103							))
104							.note(format!(
105								"requirement '{}' found in {}",
106								dep.raw_req, dep.origin,
107							))
108							.emit(sess);
109
110						if !sess.dry {
111							if !dep.build_dep {
112								pkgdep.push(prov_pkg.clone());
113							} else {
114								builddep.push(prov_pkg.clone());
115							}
116							pkgdep_dirty = true;
117						}
118					});
119				}
120			}
121			if pkgdep_dirty {
122				apml.with_upgraded(|apml| {
123					apml.with_editor(|apml| {
124						apml.replace_var_lst(
125							"PKGDEP",
126							lst::VariableValue::String(pkgdep.print().into()),
127						);
128						apml.replace_var_lst(
129							"BUILDDEP",
130							lst::VariableValue::String(builddep.print().into()),
131						);
132					})
133				});
134			}
135		}
136		Ok(())
137	}
138}