diff options
| author | Paolo Bonzini <pbonzini@redhat.com> | 2025-10-23 15:33:20 +0200 |
|---|---|---|
| committer | Xavier Claessens <xclaesse@gmail.com> | 2025-10-23 17:02:13 +0100 |
| commit | 0fcc18171b88ca5f9fa471fd204d6ee0129bd58c (patch) | |
| tree | 20efa974cffcad19776fb923ff3d73899b611c40 | |
| parent | ab26e300470a42950f95bcdc9cd38c670d7a74c7 (diff) | |
| download | meson-0fcc18171b88ca5f9fa471fd204d6ee0129bd58c.tar.gz | |
cargo: parse lints table
The lints table in Cargo.toml has a very human-targeted syntax. When
building manifest.from_raw, flatten everything into a single list,
prefixing the tool name to every warning option and sorting by priority.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
| -rw-r--r-- | mesonbuild/cargo/manifest.py | 50 | ||||
| -rw-r--r-- | mesonbuild/cargo/raw.py | 22 | ||||
| -rw-r--r-- | unittests/cargotests.py | 54 |
3 files changed, 123 insertions, 3 deletions
diff --git a/mesonbuild/cargo/manifest.py b/mesonbuild/cargo/manifest.py index 5440437c1..ec10d4611 100644 --- a/mesonbuild/cargo/manifest.py +++ b/mesonbuild/cargo/manifest.py @@ -19,7 +19,7 @@ if T.TYPE_CHECKING: from typing_extensions import Protocol, Self from . import raw - from .raw import EDITION, CRATE_TYPE + from .raw import EDITION, CRATE_TYPE, LINT_LEVEL from ..wrap.wrap import PackageDefinition # Copied from typeshed. Blarg that they don't expose this @@ -420,6 +420,41 @@ class Example(BuildTarget): @dataclasses.dataclass +class Lint: + + """Cargo Lint definition. + """ + + name: str + level: LINT_LEVEL + priority: int + check_cfg: T.Optional[T.List[str]] + + @classmethod + def from_raw(cls, r: T.Union[raw.FromWorkspace, T.Dict[str, T.Dict[str, raw.LintV]]]) -> T.List[Lint]: + r = T.cast('T.Dict[str, T.Dict[str, raw.LintV]]', r) + lints: T.Dict[str, Lint] = {} + for tool, raw_lints in r.items(): + prefix = '' if tool == 'rust' else f'{tool}::' + for name, settings in raw_lints.items(): + name = prefix + name + if isinstance(settings, str): + settings = T.cast('raw.Lint', {'level': settings}) + check_cfg = None + if name == 'unexpected_cfgs': + # 'cfg(test)' is added automatically by cargo + check_cfg = ['cfg(test)'] + settings.get('check-cfg', []) + lints[name] = Lint(name=name, + level=settings['level'], + priority=settings.get('priority', 0), + check_cfg=check_cfg) + + lints_final = list(lints.values()) + lints_final.sort(key=lambda x: x.priority) + return lints_final + + +@dataclasses.dataclass class Manifest: """Cargo Manifest definition. @@ -444,6 +479,7 @@ class Manifest: example: T.List[Example] = dataclasses.field(default_factory=list) features: T.Dict[str, T.List[str]] = dataclasses.field(default_factory=dict) target: T.Dict[str, T.Dict[str, Dependency]] = dataclasses.field(default_factory=dict) + lints: T.List[Lint] = dataclasses.field(default_factory=list) # missing: profile @@ -466,11 +502,13 @@ class Manifest: return {k: Dependency.from_raw(k, v, member_path, workspace) for k, v in x.items()} return _raw_to_dataclass(raw, cls, f'Cargo.toml package {pkg.name}', + raw_from_workspace=workspace.inheritable if workspace else None, ignored_fields=['badges', 'workspace'], package=ConvertValue(lambda _: pkg), dependencies=ConvertValue(dependencies_from_raw), dev_dependencies=ConvertValue(dependencies_from_raw), build_dependencies=ConvertValue(dependencies_from_raw), + lints=ConvertValue(Lint.from_raw), lib=ConvertValue(lambda x: Library.from_raw(x, pkg), default=autolib), bin=ConvertValue(lambda x: [Binary.from_raw(b, pkg) for b in x]), test=ConvertValue(lambda x: [Test.from_raw(b, pkg) for b in x]), @@ -493,12 +531,20 @@ class Workspace: # inheritable settings are kept in raw format, for use with _raw_to_dataclass package: T.Optional[raw.Package] = None dependencies: T.Dict[str, raw.Dependency] = dataclasses.field(default_factory=dict) - lints: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict) + lints: T.Dict[str, T.Dict[str, raw.LintV]] = dataclasses.field(default_factory=dict) metadata: T.Dict[str, T.Any] = dataclasses.field(default_factory=dict) # A workspace can also have a root package. root_package: T.Optional[Manifest] = None + @lazy_property + def inheritable(self) -> T.Dict[str, object]: + # the whole lints table is inherited. Do not add package, dependencies + # etc. because they can only be inherited a field at a time. + return { + 'lints': self.lints, + } + @classmethod def from_raw(cls, raw: raw.Manifest, path: str) -> Self: ws = _raw_to_dataclass(raw['workspace'], cls, 'Workspace') diff --git a/mesonbuild/cargo/raw.py b/mesonbuild/cargo/raw.py index 3b710d022..b683f06d8 100644 --- a/mesonbuild/cargo/raw.py +++ b/mesonbuild/cargo/raw.py @@ -10,6 +10,7 @@ from typing_extensions import Literal, TypedDict, Required EDITION = Literal['2015', '2018', '2021'] CRATE_TYPE = Literal['bin', 'lib', 'dylib', 'staticlib', 'cdylib', 'rlib', 'proc-macro'] +LINT_LEVEL = Literal['allow', 'deny', 'forbid', 'warn'] class FromWorkspace(TypedDict): @@ -121,6 +122,26 @@ class Target(TypedDict): dependencies: T.Dict[str, T.Union[FromWorkspace, DependencyV]] +Lint = TypedDict( + 'Lint', + { + 'level': Required[LINT_LEVEL], + 'priority': int, + 'check-cfg': T.List[str], + }, + total=True, +) +"""The representation of a linter setting. + +This does not include the name or tool, since those are the keys of the +dictionaries that point to Lint. +""" + + +LintV = T.Union[Lint, str] +"""A Lint entry, either a string or a Lint Dict.""" + + class Workspace(TypedDict): """The representation of a workspace. @@ -154,6 +175,7 @@ Manifest = TypedDict( 'features': T.Dict[str, T.List[str]], 'target': T.Dict[str, Target], 'workspace': Workspace, + 'lints': T.Union[FromWorkspace, T.Dict[str, T.Dict[str, LintV]]], # TODO: patch? # TODO: replace? diff --git a/unittests/cargotests.py b/unittests/cargotests.py index 3dd809dca..0c2b645f5 100644 --- a/unittests/cargotests.py +++ b/unittests/cargotests.py @@ -11,7 +11,7 @@ import typing as T from mesonbuild.cargo import cfg from mesonbuild.cargo.cfg import TokenType from mesonbuild.cargo.interpreter import load_cargo_lock -from mesonbuild.cargo.manifest import Dependency, Manifest, Package, Workspace +from mesonbuild.cargo.manifest import Dependency, Lint, Manifest, Package, Workspace from mesonbuild.cargo.toml import load_toml from mesonbuild.cargo.version import convert @@ -246,6 +246,13 @@ class CargoTomlTest(unittest.TestCase): async-channel = "2.0" zerocopy = { version = "0.7", features = ["derive"] } + [lints.rust] + unknown_lints = "allow" + unexpected_cfgs = { level = "deny", check-cfg = [ 'cfg(MESON)' ] } + + [lints.clippy] + pedantic = {level = "warn", priority = -1} + [dev-dependencies.gir-format-check] version = "^0.1" ''') @@ -308,8 +315,29 @@ class CargoTomlTest(unittest.TestCase): gtk = { package = "gtk4", version = "0.9" } once_cell = "1.0" syn = { version = "2", features = ["parse"] } + + [workspace.lints.rust] + warnings = "deny" ''') + def test_cargo_toml_ws_lints(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + fname = os.path.join(tmpdir, 'Cargo.toml') + with open(fname, 'w', encoding='utf-8') as f: + f.write(self.CARGO_TOML_WS) + workspace_toml = load_toml(fname) + + workspace = Workspace.from_raw(workspace_toml, tmpdir) + pkg = Manifest.from_raw({'package': {'name': 'foo'}, + 'lints': {'workspace': True}}, 'Cargo.toml', workspace) + lints = pkg.lints + self.assertEqual(lints[0].name, 'warnings') + self.assertEqual(lints[0].level, 'deny') + self.assertEqual(lints[0].priority, 0) + + pkg = Manifest.from_raw({'package': {'name': 'bar'}}, 'Cargo.toml', workspace) + self.assertEqual(pkg.lints, []) + def test_cargo_toml_ws_package(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: fname = os.path.join(tmpdir, 'Cargo.toml') @@ -378,6 +406,30 @@ class CargoTomlTest(unittest.TestCase): print(manifest.package.metadata) self.assertEqual(len(manifest.package.metadata), 1) + def test_cargo_toml_lints(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + fname = os.path.join(tmpdir, 'Cargo.toml') + with open(fname, 'w', encoding='utf-8') as f: + f.write(self.CARGO_TOML_1) + manifest_toml = load_toml(fname) + manifest = Manifest.from_raw(manifest_toml, 'Cargo.toml') + + self.assertEqual(len(manifest.lints), 3) + self.assertEqual(manifest.lints[0].name, 'clippy::pedantic') + self.assertEqual(manifest.lints[0].level, 'warn') + self.assertEqual(manifest.lints[0].priority, -1) + self.assertEqual(manifest.lints[0].check_cfg, None) + + self.assertEqual(manifest.lints[1].name, 'unknown_lints') + self.assertEqual(manifest.lints[1].level, 'allow') + self.assertEqual(manifest.lints[1].priority, 0) + self.assertEqual(manifest.lints[1].check_cfg, None) + + self.assertEqual(manifest.lints[2].name, 'unexpected_cfgs') + self.assertEqual(manifest.lints[2].level, 'deny') + self.assertEqual(manifest.lints[2].priority, 0) + self.assertEqual(manifest.lints[2].check_cfg, ['cfg(test)', 'cfg(MESON)']) + def test_cargo_toml_dependencies(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: fname = os.path.join(tmpdir, 'Cargo.toml') |
