aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-21 15:10:38 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-21 15:12:52 +0800
commit9976b31a56bcc3ed37ac878b351bd8d097dc67cc (patch)
treee7b092aee5e82f5d4c1f50332a17f1d2176ae51b /mingling_cli
parentee01756279ef17487d19659adedc0639547bbd25 (diff)
feat(mling): add linter framework with async support and registry
generation
Diffstat (limited to 'mingling_cli')
-rw-r--r--mingling_cli/.gitignore1
-rw-r--r--mingling_cli/Cargo.lock324
-rw-r--r--mingling_cli/Cargo.toml14
-rw-r--r--mingling_cli/about_mling_lint.md35
-rw-r--r--mingling_cli/build.rs7
-rw-r--r--mingling_cli/expand.rs1121
-rw-r--r--mingling_cli/pre/lint_registry.rs192
-rw-r--r--mingling_cli/pre/mod.rs2
-rw-r--r--mingling_cli/src/diagnostic.rs99
-rw-r--r--mingling_cli/src/errors.rs1
-rw-r--r--mingling_cli/src/errors/serde_json.rs8
-rw-r--r--mingling_cli/src/linter.rs2
-rw-r--r--mingling_cli/src/linter/cmd_mlint.rs119
-rw-r--r--mingling_cli/src/linter/mlint_attr.rs39
-rw-r--r--mingling_cli/src/linter/mlint_report.rs413
-rw-r--r--mingling_cli/src/lints.rs61
-rw-r--r--mingling_cli/src/lints/template_linter.rs48
-rw-r--r--mingling_cli/src/lints/unnecessary_render_result_creation.rs157
-rw-r--r--mingling_cli/src/main.rs15
-rw-r--r--mingling_cli/src/message.rs10
-rw-r--r--mingling_cli/src/metadata.rs14
-rw-r--r--mingling_cli/src/metadata/cmd_metadata.rs9
-rw-r--r--mingling_cli/src/metadata/setup.rs30
-rw-r--r--mingling_cli/tmpls/lints.tmpl56
24 files changed, 2746 insertions, 31 deletions
diff --git a/mingling_cli/.gitignore b/mingling_cli/.gitignore
new file mode 100644
index 0000000..6a6d575
--- /dev/null
+++ b/mingling_cli/.gitignore
@@ -0,0 +1 @@
+registry.json
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock
index e6098d5..6d34351 100644
--- a/mingling_cli/Cargo.lock
+++ b/mingling_cli/Cargo.lock
@@ -3,11 +3,28 @@
version = 4
[[package]]
+name = "annotate-snippets"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1"
+dependencies = [
+ "anstyle",
+ "memchr",
+ "unicode-width",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
name = "arg-picker"
version = "0.1.0"
dependencies = [
"arg-picker-macros",
- "just_fmt",
+ "just_fmt 0.2.0",
]
[[package]]
@@ -20,6 +37,18 @@ dependencies = [
]
[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
name = "camino"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -46,6 +75,7 @@ checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
dependencies = [
"camino",
"cargo-platform",
+ "derive_builder",
"semver",
"serde",
"serde_json",
@@ -53,6 +83,100 @@ dependencies = [
]
[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -60,11 +184,53 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_fmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
+name = "just_template"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
+dependencies = [
+ "just_fmt 0.1.2",
+ "just_template_macros",
+]
+
+[[package]]
+name = "just_template_macros"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1471eb68722ecefeb71debdde2859e8725341f171d3f42b3a98a0862ad19416e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.187"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7743783ea728ef5c31194c6590797eed286449b4a4e87d626d8a51f0a94e732"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -77,34 +243,36 @@ dependencies = [
"arg-picker",
"mingling_core",
"mingling_macros",
- "serde",
]
[[package]]
name = "mingling-cli"
version = "0.3.0"
dependencies = [
+ "annotate-snippets",
"cargo_metadata",
+ "just_template",
"mingling",
+ "proc-macro2",
"serde",
"serde_json",
+ "syn 3.0.2",
+ "tokio",
]
[[package]]
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"mingling_pathf",
- "serde",
- "serde_json",
]
[[package]]
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -114,12 +282,52 @@ dependencies = [
name = "mingling_pathf"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"syn 2.0.119",
]
[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -138,6 +346,21 @@ dependencies = [
]
[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -191,6 +414,38 @@ dependencies = [
]
[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -233,12 +488,67 @@ dependencies = [
]
[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml
index 93021f0..bc8d59f 100644
--- a/mingling_cli/Cargo.toml
+++ b/mingling_cli/Cargo.toml
@@ -21,7 +21,7 @@ features = [
"picker",
"pathf",
"extra_macros",
- "structural_renderer"
+ "async",
]
[build-dependencies.mingling]
@@ -32,8 +32,18 @@ features = [
]
[dependencies]
-cargo_metadata = "0.23.1"
+annotate-snippets = "0.12.16"
+cargo_metadata = { version = "0.23.1", features = ["builder"] }
+just_template = "0.2.0"
+proc-macro2 = { version = "1.0.107", features = ["span-locations"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
+syn = { version = "3.0.2", features = ["full", "extra-traits"] }
+tokio = { version = "1.53.1", features = ["full"] }
+
+[build-dependencies]
+serde = { version = "1.0.229", features = ["derive"] }
+serde_json = "1.0.151"
+just_template = "0.2.0"
[workspace]
diff --git a/mingling_cli/about_mling_lint.md b/mingling_cli/about_mling_lint.md
new file mode 100644
index 0000000..87fedc8
--- /dev/null
+++ b/mingling_cli/about_mling_lint.md
@@ -0,0 +1,35 @@
+# 关于 Mingling Linter 设计
+
+Mingling Linter 是独立于 `clippy` 和 `check` 之外的检查器,用于检查 Mingling 项目的代码质量
+
+## 关于 Linter 定义
+
+已完成!所有的定义都被输出到 registry.json 中,可以读取
+
+## 忽略规则
+
+Linter 在检查时,会检查开头的 mlint 标识,用局部读写直到没有#![.*]这样的结构为止,判断当前文件需要参与的 Linter
+
+默认来讲,一个文件所有默认为 deny、warn 的 linter 都会参与,直到被显式 allow 覆盖。
+
+```rust
+#![mlint(allow(linter))]
+#![mlint(warn(linter))]
+#![mlint(deny(linter))]
+```
+
+## 检查阶段
+
+1. 为每个包的内部 rs 文件(必须被包含)检查,并按照忽略规则,计算每个文件需要哪些 linter 检查(仅 file 级别的)
+2. 使用 tokio 并行检查
+3. 同时,在检查过程中,计算更细粒度的哪些 Linter 需要在哪些行(或 AST 层级,待定)检查,检查的程度是什么 (warn还是deny)
+4. 使用 tokio 并行检查 所有 细粒度的 Linter
+5. 输出检查结果
+
+## 性能
+
+请使用 tokio 并行化执行 linter,但是结果放在缓冲区,计算完成后按照文件层级(主:文件深度,次:名字 a-Z,辅:名字 0-9)排序,后输出
+
+## 关于架构层面
+
+第一版可以直接使用 tokio 并行化,这不会很难
diff --git a/mingling_cli/build.rs b/mingling_cli/build.rs
index 2a36e37..b74d8a0 100644
--- a/mingling_cli/build.rs
+++ b/mingling_cli/build.rs
@@ -1,5 +1,12 @@
use mingling::build::analyze_and_build_type_mapping;
+pub mod pre;
+
fn main() {
+ // Perform path analysis and build type mapping table
analyze_and_build_type_mapping().unwrap();
+
+ // Generate lint registry
+ pre::gen_mod_file().unwrap();
+ pre::gen_lint_registry().unwrap();
}
diff --git a/mingling_cli/expand.rs b/mingling_cli/expand.rs
new file mode 100644
index 0000000..2e8bc4f
--- /dev/null
+++ b/mingling_cli/expand.rs
@@ -0,0 +1,1121 @@
+#![feature(prelude_import)]
+extern crate std;
+#[prelude_import]
+use std::prelude::rust_2024::*;
+use crate::{linter::MinglingLinterSetup, metadata::MinglingMetadataSetup};
+use mingling::{
+ macros::gen_program,
+ setup::{ExitCodeSetup, picker::{HelpFlagSetup, StructuralRendererSetup}},
+};
+pub mod errors {
+ pub mod serde_json {
+ use mingling::macros::{buffer, group, r_println, renderer};
+ pub type ErrorSerdeJson = serde_json::Error;
+ #[allow(non_camel_case_types)]
+ mod internal_group_serde_json_error {
+ use crate::ThisProgram as __MinglingProgram;
+ #[allow(unused_imports)]
+ use serde_json::Error;
+ use super::ErrorSerdeJson;
+ impl ::mingling::Grouped<__MinglingProgram> for ErrorSerdeJson {
+ fn member_id() -> __MinglingProgram {
+ __MinglingProgram::ErrorSerdeJson
+ }
+ }
+ }
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ pub struct __internal_renderer_render_error_serde_json;
+ impl ::mingling::Renderer for __internal_renderer_render_error_serde_json {
+ type Previous = ErrorSerdeJson;
+ fn render(prev: Self::Previous) -> ::mingling::RenderResult {
+ let __renderer_result = { render_error_serde_json(prev) };
+ ::std::convert::Into::into(__renderer_result)
+ }
+ }
+ pub fn render_error_serde_json(
+ _err: ErrorSerdeJson,
+ ) -> ::mingling::RenderResult {
+ let mut __render_result_buffer = ::mingling::RenderResult::new();
+ {
+ __render_result_buffer
+ .println(
+ ::alloc::__export::must_use({
+ ::alloc::fmt::format(format_args!("serde"))
+ }),
+ );
+ }
+ __render_result_buffer
+ }
+ }
+}
+pub mod linter {
+ use mingling::{Program, macros::program_setup};
+ use crate::linter::cmd_mlint::CMDMinglingLinter;
+ pub mod cmd_mlint {
+ use mingling::macros::{chain, dispatcher};
+ pub struct CMDMinglingLinter;
+ #[automatically_derived]
+ impl ::core::fmt::Debug for CMDMinglingLinter {
+ #[inline]
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ ::core::fmt::Formatter::write_str(f, "CMDMinglingLinter")
+ }
+ }
+ #[automatically_derived]
+ impl ::core::default::Default for CMDMinglingLinter {
+ #[inline]
+ fn default() -> CMDMinglingLinter {
+ CMDMinglingLinter {}
+ }
+ }
+ pub struct EntryMinglingLinter {
+ pub(crate) inner: Vec<String>,
+ }
+ impl EntryMinglingLinter {
+ /// Creates a new instance of the wrapper type
+ pub fn new(inner: Vec<String>) -> Self {
+ Self { inner }
+ }
+ }
+ impl From<Vec<String>> for EntryMinglingLinter {
+ fn from(inner: Vec<String>) -> Self {
+ Self::new(inner)
+ }
+ }
+ impl From<EntryMinglingLinter> for Vec<String> {
+ fn from(wrapper: EntryMinglingLinter) -> Vec<String> {
+ wrapper.inner
+ }
+ }
+ impl ::std::convert::AsRef<Vec<String>> for EntryMinglingLinter {
+ fn as_ref(&self) -> &Vec<String> {
+ &self.inner
+ }
+ }
+ impl ::std::convert::AsMut<Vec<String>> for EntryMinglingLinter {
+ fn as_mut(&mut self) -> &mut Vec<String> {
+ &mut self.inner
+ }
+ }
+ impl ::std::ops::Deref for EntryMinglingLinter {
+ type Target = Vec<String>;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+ }
+ impl ::std::ops::DerefMut for EntryMinglingLinter {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+ }
+ impl ::std::default::Default for EntryMinglingLinter
+ where
+ Vec<String>: ::std::default::Default,
+ {
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+ }
+ impl Into<mingling::AnyOutput<crate::ThisProgram>> for EntryMinglingLinter {
+ fn into(self) -> mingling::AnyOutput<crate::ThisProgram> {
+ mingling::AnyOutput::new(self)
+ }
+ }
+ impl Into<mingling::ChainProcess<crate::ThisProgram>> for EntryMinglingLinter {
+ fn into(self) -> mingling::ChainProcess<crate::ThisProgram> {
+ mingling::AnyOutput::new(self).route_chain()
+ }
+ }
+ impl ::mingling::Grouped<crate::ThisProgram> for EntryMinglingLinter {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::EntryMinglingLinter
+ }
+ }
+ impl ::mingling::Dispatcher<crate::ThisProgram> for CMDMinglingLinter {
+ fn node(&self) -> ::mingling::Node {
+ mingling::Node::default().join("mlint")
+ }
+ fn begin(
+ &self,
+ args: Vec<String>,
+ ) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ use ::mingling::Grouped;
+ ::mingling::Routable::to_chain(EntryMinglingLinter::new(args))
+ }
+ fn clone_dispatcher(
+ &self,
+ ) -> Box<dyn ::mingling::Dispatcher<crate::ThisProgram>> {
+ Box::new(CMDMinglingLinter)
+ }
+ }
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ pub struct __internal_chain_handle_mlint;
+ impl ::mingling::Chain<crate::ThisProgram> for __internal_chain_handle_mlint {
+ type Previous = EntryMinglingLinter;
+ fn proc(
+ prev: EntryMinglingLinter,
+ ) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ handle_mlint(prev);
+ <crate::ResultEmpty as ::mingling::Routable<
+ crate::ThisProgram,
+ >>::to_chain(crate::ResultEmpty)
+ }
+ }
+ pub fn handle_mlint(_args: EntryMinglingLinter) {}
+ }
+ #[doc(hidden)]
+ pub struct MinglingLinterSetup;
+ impl ::mingling::setup::ProgramSetup<crate::ThisProgram> for MinglingLinterSetup {
+ fn setup(self, program: &mut ::mingling::Program<crate::ThisProgram>) {
+ mingling_linter_setup(program);
+ }
+ }
+ pub fn mingling_linter_setup(program: &mut Program<crate::ThisProgram>) {
+ {
+ program.with_setup(MinglingLinterCommandSetup);
+ }
+ }
+ #[doc(hidden)]
+ pub struct MinglingLinterCommandSetup;
+ impl ::mingling::setup::ProgramSetup<crate::ThisProgram>
+ for MinglingLinterCommandSetup {
+ fn setup(self, program: &mut ::mingling::Program<crate::ThisProgram>) {
+ mingling_linter_command_setup(program);
+ }
+ }
+ pub fn mingling_linter_command_setup(program: &mut Program<crate::ThisProgram>) {
+ {
+ program.with_dispatcher(CMDMinglingLinter);
+ }
+ }
+}
+pub mod metadata {
+ use cargo_metadata::Metadata;
+ use mingling::{
+ AnyOutput, Program, ProgramCollect, RenderResult,
+ macros::{buffer, group_structural, program_setup, r_println, renderer, routeify},
+ };
+ use crate::metadata::{cmd_metadata::CMDMetadata, setup::CargoMetadataSetup};
+ pub mod cmd_metadata {
+ use cargo_metadata::Metadata;
+ use mingling::{LazyRes, macros::{chain, dispatcher, pack_structural}};
+ use crate::metadata::setup::ResMetadata;
+ pub struct CMDMetadata;
+ #[automatically_derived]
+ impl ::core::fmt::Debug for CMDMetadata {
+ #[inline]
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ ::core::fmt::Formatter::write_str(f, "CMDMetadata")
+ }
+ }
+ #[automatically_derived]
+ impl ::core::default::Default for CMDMetadata {
+ #[inline]
+ fn default() -> CMDMetadata {
+ CMDMetadata {}
+ }
+ }
+ pub struct EntryMetadata {
+ pub(crate) inner: Vec<String>,
+ }
+ impl EntryMetadata {
+ /// Creates a new instance of the wrapper type
+ pub fn new(inner: Vec<String>) -> Self {
+ Self { inner }
+ }
+ }
+ impl From<Vec<String>> for EntryMetadata {
+ fn from(inner: Vec<String>) -> Self {
+ Self::new(inner)
+ }
+ }
+ impl From<EntryMetadata> for Vec<String> {
+ fn from(wrapper: EntryMetadata) -> Vec<String> {
+ wrapper.inner
+ }
+ }
+ impl ::std::convert::AsRef<Vec<String>> for EntryMetadata {
+ fn as_ref(&self) -> &Vec<String> {
+ &self.inner
+ }
+ }
+ impl ::std::convert::AsMut<Vec<String>> for EntryMetadata {
+ fn as_mut(&mut self) -> &mut Vec<String> {
+ &mut self.inner
+ }
+ }
+ impl ::std::ops::Deref for EntryMetadata {
+ type Target = Vec<String>;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+ }
+ impl ::std::ops::DerefMut for EntryMetadata {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+ }
+ impl ::std::default::Default for EntryMetadata
+ where
+ Vec<String>: ::std::default::Default,
+ {
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+ }
+ impl Into<mingling::AnyOutput<crate::ThisProgram>> for EntryMetadata {
+ fn into(self) -> mingling::AnyOutput<crate::ThisProgram> {
+ mingling::AnyOutput::new(self)
+ }
+ }
+ impl Into<mingling::ChainProcess<crate::ThisProgram>> for EntryMetadata {
+ fn into(self) -> mingling::ChainProcess<crate::ThisProgram> {
+ mingling::AnyOutput::new(self).route_chain()
+ }
+ }
+ impl ::mingling::Grouped<crate::ThisProgram> for EntryMetadata {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::EntryMetadata
+ }
+ }
+ impl ::mingling::Dispatcher<crate::ThisProgram> for CMDMetadata {
+ fn node(&self) -> ::mingling::Node {
+ mingling::Node::default().join("metadata")
+ }
+ fn begin(
+ &self,
+ args: Vec<String>,
+ ) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ use ::mingling::Grouped;
+ ::mingling::Routable::to_chain(EntryMetadata::new(args))
+ }
+ fn clone_dispatcher(
+ &self,
+ ) -> Box<dyn ::mingling::Dispatcher<crate::ThisProgram>> {
+ Box::new(CMDMetadata)
+ }
+ }
+ pub struct ResultMetadata {
+ pub inner: ResMetadata,
+ }
+ #[doc(hidden)]
+ #[allow(
+ non_upper_case_globals,
+ unused_attributes,
+ unused_qualifications,
+ clippy::absolute_paths,
+ )]
+ const _: () = {
+ #[allow(unused_extern_crates, clippy::useless_attribute)]
+ extern crate serde as _serde;
+ #[automatically_derived]
+ impl _serde::Serialize for ResultMetadata {
+ fn serialize<__S>(
+ &self,
+ __serializer: __S,
+ ) -> _serde::__private229::Result<__S::Ok, __S::Error>
+ where
+ __S: _serde::Serializer,
+ {
+ let mut __serde_state = _serde::Serializer::serialize_struct(
+ __serializer,
+ "ResultMetadata",
+ false as usize + 1,
+ )?;
+ _serde::ser::SerializeStruct::serialize_field(
+ &mut __serde_state,
+ "inner",
+ &self.inner,
+ )?;
+ _serde::ser::SerializeStruct::end(__serde_state)
+ }
+ }
+ };
+ impl ResultMetadata {
+ pub fn new(inner: ResMetadata) -> Self {
+ Self { inner }
+ }
+ }
+ impl From<ResMetadata> for ResultMetadata {
+ fn from(inner: ResMetadata) -> Self {
+ Self::new(inner)
+ }
+ }
+ impl From<ResultMetadata> for ResMetadata {
+ fn from(wrapper: ResultMetadata) -> ResMetadata {
+ wrapper.inner
+ }
+ }
+ impl ::std::convert::AsRef<ResMetadata> for ResultMetadata {
+ fn as_ref(&self) -> &ResMetadata {
+ &self.inner
+ }
+ }
+ impl ::std::convert::AsMut<ResMetadata> for ResultMetadata {
+ fn as_mut(&mut self) -> &mut ResMetadata {
+ &mut self.inner
+ }
+ }
+ impl ::std::ops::Deref for ResultMetadata {
+ type Target = ResMetadata;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+ }
+ impl ::std::ops::DerefMut for ResultMetadata {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+ }
+ impl ::std::default::Default for ResultMetadata
+ where
+ ResMetadata: ::std::default::Default,
+ {
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+ }
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram>
+ for ResultMetadata {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram>
+ for ResultMetadata {}
+ impl Into<::mingling::AnyOutput<crate::ThisProgram>> for ResultMetadata {
+ fn into(self) -> ::mingling::AnyOutput<crate::ThisProgram> {
+ ::mingling::AnyOutput::new(self)
+ }
+ }
+ impl Into<::mingling::ChainProcess<crate::ThisProgram>> for ResultMetadata {
+ fn into(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ ::mingling::AnyOutput::new(self).route_chain()
+ }
+ }
+ impl ::mingling::Grouped<crate::ThisProgram> for ResultMetadata {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::ResultMetadata
+ }
+ }
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ pub struct __internal_chain_handle_metadata;
+ impl ::mingling::Chain<crate::ThisProgram> for __internal_chain_handle_metadata {
+ type Previous = EntryMetadata;
+ fn proc(
+ prev: EntryMetadata,
+ ) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ let __chain_result = {
+ ::mingling::this::<crate::ThisProgram>()
+ .__modify_res_and_return_route(|
+ metadata: &mut LazyRes<ResMetadata>|
+ {
+ ::mingling::Routable::<
+ crate::ThisProgram,
+ >::to_chain(handle_metadata(prev, metadata))
+ })
+ };
+ ::mingling::Routable::<crate::ThisProgram>::to_chain(__chain_result)
+ }
+ }
+ pub fn handle_metadata(
+ _: EntryMetadata,
+ metadata: &mut LazyRes<ResMetadata>,
+ ) -> Metadata {
+ let metadata = metadata.get_ref().clone();
+ metadata.data().clone()
+ }
+ }
+ pub mod setup {
+ use std::{env::current_dir, path::PathBuf};
+ use cargo_metadata::{CargoOpt, MetadataCommand};
+ use mingling::{
+ LazyInit, Program, consts::REMAINS, macros::program_setup,
+ picker::{IntoPicker, value::Flag},
+ prelude::*, res::ResCurrentDir,
+ };
+ /// Name of Cargo manifest file
+ pub const CARGO_TOML: &str = "Cargo.toml";
+ use cargo_metadata::Metadata;
+ use serde::Serialize;
+ pub struct ResMetadata {
+ data: Option<Metadata>,
+ }
+ #[automatically_derived]
+ impl ::core::default::Default for ResMetadata {
+ #[inline]
+ fn default() -> ResMetadata {
+ ResMetadata {
+ data: ::core::default::Default::default(),
+ }
+ }
+ }
+ #[automatically_derived]
+ impl ::core::clone::Clone for ResMetadata {
+ #[inline]
+ fn clone(&self) -> ResMetadata {
+ ResMetadata {
+ data: ::core::clone::Clone::clone(&self.data),
+ }
+ }
+ }
+ #[doc(hidden)]
+ #[allow(
+ non_upper_case_globals,
+ unused_attributes,
+ unused_qualifications,
+ clippy::absolute_paths,
+ )]
+ const _: () = {
+ #[allow(unused_extern_crates, clippy::useless_attribute)]
+ extern crate serde as _serde;
+ #[automatically_derived]
+ impl _serde::Serialize for ResMetadata {
+ fn serialize<__S>(
+ &self,
+ __serializer: __S,
+ ) -> _serde::__private229::Result<__S::Ok, __S::Error>
+ where
+ __S: _serde::Serializer,
+ {
+ let mut __serde_state = _serde::Serializer::serialize_struct(
+ __serializer,
+ "ResMetadata",
+ false as usize + 1,
+ )?;
+ _serde::ser::SerializeStruct::serialize_field(
+ &mut __serde_state,
+ "data",
+ &self.data,
+ )?;
+ _serde::ser::SerializeStruct::end(__serde_state)
+ }
+ }
+ };
+ impl ResMetadata {
+ /// Returns a reference to the parsed `cargo metadata`.
+ ///
+ /// # Panics
+ ///
+ /// This function does **not** panic in practice, because `ResMetadata` is
+ /// always initialized via [`LazyInit`] inside the program setup, and the
+ /// initialization either succeeds (setting `data` to `Some(...)`) or fails
+ /// by propagating the error from `cmd.exec().unwrap()`. Therefore, by the
+ /// time this getter is called, `self.data` is guaranteed to be `Some`.
+ pub fn data(&self) -> &Metadata {
+ self.data.as_ref().unwrap()
+ }
+ }
+ #[doc(hidden)]
+ pub struct CargoMetadataSetup;
+ impl ::mingling::setup::ProgramSetup<crate::ThisProgram> for CargoMetadataSetup {
+ fn setup(self, program: &mut ::mingling::Program<crate::ThisProgram>) {
+ cargo_metadata_setup(program);
+ }
+ }
+ pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) {
+ {
+ let args = program.take_args();
+ let (
+ feature_args,
+ manifest_path,
+ message_format,
+ enable_all_features,
+ no_default_features,
+ no_deps,
+ args,
+ ) = args
+ .pick(
+ &::mingling::picker::PickerArg::<Vec<String>> {
+ full: &["features"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ )
+ .pick(
+ &::mingling::picker::PickerArg::<Option<String>> {
+ full: &["manifest_path"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ )
+ .pick_or(
+ &::mingling::picker::PickerArg::<String> {
+ full: &["message_format"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ || "disable".to_string(),
+ )
+ .pick(
+ &::mingling::picker::PickerArg::<Flag> {
+ full: &["all_features"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ )
+ .pick(
+ &::mingling::picker::PickerArg::<Flag> {
+ full: &["no_default_features"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ )
+ .pick(
+ &::mingling::picker::PickerArg::<Flag> {
+ full: &["no_deps"],
+ short: ::std::option::Option::None,
+ positional: false,
+ internal_type: ::std::marker::PhantomData,
+ },
+ )
+ .pick(&REMAINS)
+ .unwrap();
+ program.replace_args(args.into());
+ program.structural_renderer_name = message_format.into();
+ let current_dir = current_dir().unwrap();
+ let feature_args_leaked: &[String] = Box::leak(
+ feature_args.into_boxed_slice(),
+ );
+ let manifest_path_clone = manifest_path.clone();
+ let current_dir_clone = current_dir.clone();
+ program
+ .with_resource(
+ ResMetadata::lazy_init(move || {
+ let metadata_path = match manifest_path_clone {
+ Some(ref path_str) => PathBuf::from(path_str),
+ None => find_manifest().unwrap(),
+ };
+ let mut cmd = MetadataCommand::new();
+ cmd.manifest_path(metadata_path)
+ .current_dir(&current_dir_clone);
+ if *enable_all_features {
+ cmd.features(CargoOpt::AllFeatures);
+ }
+ if *no_default_features {
+ cmd.features(CargoOpt::NoDefaultFeatures);
+ }
+ if *no_deps {
+ cmd.no_deps();
+ }
+ cmd.features(
+ CargoOpt::SomeFeatures(feature_args_leaked.to_vec()),
+ );
+ ResMetadata {
+ data: Some(cmd.exec().unwrap()),
+ }
+ }),
+ );
+ program.with_resource(ResCurrentDir::from(current_dir));
+ }
+ }
+ /// Find `Cargo.toml` by searching upward from `current_dir`.
+ fn find_manifest() -> Option<PathBuf> {
+ let mut dir = current_dir().ok()?;
+ loop {
+ let candidate = dir.join(CARGO_TOML);
+ if candidate.is_file() {
+ return Some(candidate);
+ }
+ if !dir.pop() {
+ return None;
+ }
+ }
+ }
+ }
+ #[doc(hidden)]
+ pub struct MinglingMetadataSetup;
+ impl ::mingling::setup::ProgramSetup<crate::ThisProgram> for MinglingMetadataSetup {
+ fn setup(self, program: &mut ::mingling::Program<crate::ThisProgram>) {
+ mingling_metadata_setup(program);
+ }
+ }
+ pub fn mingling_metadata_setup(program: &mut Program<crate::ThisProgram>) {
+ {
+ program.with_setup(CargoMetadataSetup);
+ program.with_dispatcher(CMDMetadata);
+ }
+ }
+ #[allow(non_camel_case_types)]
+ mod internal_group_metadata {
+ use crate::ThisProgram as __MinglingProgram;
+ #[allow(unused_imports)]
+ use super::Metadata;
+ impl ::mingling::Grouped<__MinglingProgram> for Metadata {
+ fn member_id() -> __MinglingProgram {
+ __MinglingProgram::Metadata
+ }
+ }
+ impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram>
+ for Metadata {}
+ impl ::mingling::__private::StructuralData<crate::ThisProgram> for Metadata {}
+ }
+ #[doc(hidden)]
+ #[allow(non_camel_case_types)]
+ pub struct __internal_renderer_render_metadata;
+ impl ::mingling::Renderer for __internal_renderer_render_metadata {
+ type Previous = Metadata;
+ fn render(prev: Self::Previous) -> ::mingling::RenderResult {
+ let __renderer_result = { render_metadata(prev) };
+ ::std::convert::Into::into(__renderer_result)
+ }
+ }
+ pub fn render_metadata(metadata: Metadata) -> RenderResult {
+ let mut r = RenderResult::new();
+ r.println(
+ ::alloc::__export::must_use({
+ ::alloc::fmt::format(
+ format_args!("{0}", serde_json::to_string(&metadata)?),
+ )
+ }),
+ );
+ r
+ }
+}
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(HelpFlagSetup::default());
+ program.with_setup(ExitCodeSetup::default());
+ program.with_setup(StructuralRendererSetup);
+ program.with_setup(MinglingMetadataSetup);
+ program.with_setup(MinglingLinterSetup);
+ program.exec_and_exit();
+}
+/// Alias for the current program type `crate::ThisProgram`
+pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
+impl ::mingling::Routable<crate::ThisProgram>
+for ::mingling::ChainProcess<crate::ThisProgram> {
+ fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
+ }
+ }
+ fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
+ }
+}
+pub struct ErrorRendererNotFound {
+ pub(crate) inner: String,
+}
+impl ErrorRendererNotFound {
+ /// Creates a new instance of the wrapper type
+ pub fn new(inner: String) -> Self {
+ Self { inner }
+ }
+}
+impl From<String> for ErrorRendererNotFound {
+ fn from(inner: String) -> Self {
+ Self::new(inner)
+ }
+}
+impl From<ErrorRendererNotFound> for String {
+ fn from(wrapper: ErrorRendererNotFound) -> String {
+ wrapper.inner
+ }
+}
+impl ::std::convert::AsRef<String> for ErrorRendererNotFound {
+ fn as_ref(&self) -> &String {
+ &self.inner
+ }
+}
+impl ::std::convert::AsMut<String> for ErrorRendererNotFound {
+ fn as_mut(&mut self) -> &mut String {
+ &mut self.inner
+ }
+}
+impl ::std::ops::Deref for ErrorRendererNotFound {
+ type Target = String;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+impl ::std::ops::DerefMut for ErrorRendererNotFound {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+impl ::std::default::Default for ErrorRendererNotFound
+where
+ String: ::std::default::Default,
+{
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+}
+impl Into<mingling::AnyOutput<crate::ThisProgram>> for ErrorRendererNotFound {
+ fn into(self) -> mingling::AnyOutput<crate::ThisProgram> {
+ mingling::AnyOutput::new(self)
+ }
+}
+impl Into<mingling::ChainProcess<crate::ThisProgram>> for ErrorRendererNotFound {
+ fn into(self) -> mingling::ChainProcess<crate::ThisProgram> {
+ mingling::AnyOutput::new(self).route_chain()
+ }
+}
+impl ::mingling::Grouped<crate::ThisProgram> for ErrorRendererNotFound {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::ErrorRendererNotFound
+ }
+}
+pub struct ErrorDispatcherNotFound {
+ pub(crate) inner: Vec<String>,
+}
+impl ErrorDispatcherNotFound {
+ /// Creates a new instance of the wrapper type
+ pub fn new(inner: Vec<String>) -> Self {
+ Self { inner }
+ }
+}
+impl From<Vec<String>> for ErrorDispatcherNotFound {
+ fn from(inner: Vec<String>) -> Self {
+ Self::new(inner)
+ }
+}
+impl From<ErrorDispatcherNotFound> for Vec<String> {
+ fn from(wrapper: ErrorDispatcherNotFound) -> Vec<String> {
+ wrapper.inner
+ }
+}
+impl ::std::convert::AsRef<Vec<String>> for ErrorDispatcherNotFound {
+ fn as_ref(&self) -> &Vec<String> {
+ &self.inner
+ }
+}
+impl ::std::convert::AsMut<Vec<String>> for ErrorDispatcherNotFound {
+ fn as_mut(&mut self) -> &mut Vec<String> {
+ &mut self.inner
+ }
+}
+impl ::std::ops::Deref for ErrorDispatcherNotFound {
+ type Target = Vec<String>;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+impl ::std::ops::DerefMut for ErrorDispatcherNotFound {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+impl ::std::default::Default for ErrorDispatcherNotFound
+where
+ Vec<String>: ::std::default::Default,
+{
+ fn default() -> Self {
+ Self::new(::std::default::Default::default())
+ }
+}
+impl Into<mingling::AnyOutput<crate::ThisProgram>> for ErrorDispatcherNotFound {
+ fn into(self) -> mingling::AnyOutput<crate::ThisProgram> {
+ mingling::AnyOutput::new(self)
+ }
+}
+impl Into<mingling::ChainProcess<crate::ThisProgram>> for ErrorDispatcherNotFound {
+ fn into(self) -> mingling::ChainProcess<crate::ThisProgram> {
+ mingling::AnyOutput::new(self).route_chain()
+ }
+}
+impl ::mingling::Grouped<crate::ThisProgram> for ErrorDispatcherNotFound {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::ErrorDispatcherNotFound
+ }
+}
+pub struct ResultEmpty;
+#[doc(hidden)]
+#[allow(
+ non_upper_case_globals,
+ unused_attributes,
+ unused_qualifications,
+ clippy::absolute_paths,
+)]
+const _: () = {
+ #[allow(unused_extern_crates, clippy::useless_attribute)]
+ extern crate serde as _serde;
+ #[automatically_derived]
+ impl _serde::Serialize for ResultEmpty {
+ fn serialize<__S>(
+ &self,
+ __serializer: __S,
+ ) -> _serde::__private229::Result<__S::Ok, __S::Error>
+ where
+ __S: _serde::Serializer,
+ {
+ _serde::Serializer::serialize_unit_struct(__serializer, "ResultEmpty")
+ }
+ }
+};
+impl ::mingling::__private::StructuralDataSealed<crate::ThisProgram> for ResultEmpty {}
+impl ::mingling::__private::StructuralData<crate::ThisProgram> for ResultEmpty {}
+impl ::mingling::Grouped<crate::ThisProgram> for ResultEmpty {
+ fn member_id() -> crate::ThisProgram {
+ crate::ThisProgram::ResultEmpty
+ }
+}
+impl ::std::convert::Into<::mingling::AnyOutput<crate::ThisProgram>> for ResultEmpty {
+ fn into(self) -> ::mingling::AnyOutput<crate::ThisProgram> {
+ ::mingling::AnyOutput::new(self)
+ }
+}
+impl ::std::convert::Into<::mingling::ChainProcess<crate::ThisProgram>> for ResultEmpty {
+ fn into(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ ::mingling::AnyOutput::new(self).route_chain()
+ }
+}
+#[automatically_derived]
+impl ::core::default::Default for ResultEmpty {
+ #[inline]
+ fn default() -> ResultEmpty {
+ ResultEmpty {}
+ }
+}
+#[repr(u8)]
+#[allow(nonstandard_style)]
+pub enum ThisProgram {
+ EntryMetadata,
+ EntryMinglingLinter,
+ ErrorDispatcherNotFound,
+ ErrorRendererNotFound,
+ ErrorSerdeJson,
+ Metadata,
+ ResultEmpty,
+ ResultMetadata,
+}
+#[automatically_derived]
+#[allow(nonstandard_style)]
+impl ::core::fmt::Debug for ThisProgram {
+ #[inline]
+ fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
+ ::core::fmt::Formatter::write_str(
+ f,
+ match self {
+ ThisProgram::EntryMetadata => "EntryMetadata",
+ ThisProgram::EntryMinglingLinter => "EntryMinglingLinter",
+ ThisProgram::ErrorDispatcherNotFound => "ErrorDispatcherNotFound",
+ ThisProgram::ErrorRendererNotFound => "ErrorRendererNotFound",
+ ThisProgram::ErrorSerdeJson => "ErrorSerdeJson",
+ ThisProgram::Metadata => "Metadata",
+ ThisProgram::ResultEmpty => "ResultEmpty",
+ ThisProgram::ResultMetadata => "ResultMetadata",
+ },
+ )
+ }
+}
+#[automatically_derived]
+#[allow(nonstandard_style)]
+impl ::core::marker::StructuralPartialEq for ThisProgram {}
+#[automatically_derived]
+#[allow(nonstandard_style)]
+impl ::core::cmp::PartialEq for ThisProgram {
+ #[inline]
+ fn eq(&self, other: &ThisProgram) -> bool {
+ let __self_discr = ::core::intrinsics::discriminant_value(self);
+ let __arg1_discr = ::core::intrinsics::discriminant_value(other);
+ __self_discr == __arg1_discr
+ }
+}
+#[automatically_derived]
+#[allow(nonstandard_style)]
+impl ::core::cmp::Eq for ThisProgram {
+ #[doc(hidden)]
+ #[coverage(off)]
+ fn assert_fields_are_eq(&self) {}
+}
+#[automatically_derived]
+#[allow(nonstandard_style)]
+impl ::core::clone::Clone for ThisProgram {
+ #[inline]
+ fn clone(&self) -> ThisProgram {
+ match self {
+ ThisProgram::EntryMetadata => ThisProgram::EntryMetadata,
+ ThisProgram::EntryMinglingLinter => ThisProgram::EntryMinglingLinter,
+ ThisProgram::ErrorDispatcherNotFound => ThisProgram::ErrorDispatcherNotFound,
+ ThisProgram::ErrorRendererNotFound => ThisProgram::ErrorRendererNotFound,
+ ThisProgram::ErrorSerdeJson => ThisProgram::ErrorSerdeJson,
+ ThisProgram::Metadata => ThisProgram::Metadata,
+ ThisProgram::ResultEmpty => ThisProgram::ResultEmpty,
+ ThisProgram::ResultMetadata => ThisProgram::ResultMetadata,
+ }
+ }
+}
+impl ::std::fmt::Display for ThisProgram {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+ match self {
+ ThisProgram::EntryMetadata => f.write_fmt(format_args!("EntryMetadata")),
+ ThisProgram::EntryMinglingLinter => {
+ f.write_fmt(format_args!("EntryMinglingLinter"))
+ }
+ ThisProgram::ErrorDispatcherNotFound => {
+ f.write_fmt(format_args!("ErrorDispatcherNotFound"))
+ }
+ ThisProgram::ErrorRendererNotFound => {
+ f.write_fmt(format_args!("ErrorRendererNotFound"))
+ }
+ ThisProgram::ErrorSerdeJson => f.write_fmt(format_args!("ErrorSerdeJson")),
+ ThisProgram::Metadata => f.write_fmt(format_args!("Metadata")),
+ ThisProgram::ResultEmpty => f.write_fmt(format_args!("ResultEmpty")),
+ ThisProgram::ResultMetadata => f.write_fmt(format_args!("ResultMetadata")),
+ }
+ }
+}
+impl ::mingling::ProgramCollect for ThisProgram {
+ type Enum = ThisProgram;
+ type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
+ type ErrorRendererNotFound = ErrorRendererNotFound;
+ type ResultEmpty = ResultEmpty;
+ fn build_renderer_not_found(
+ member_id: Self::Enum,
+ ) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
+ }
+ fn build_dispatcher_not_found(
+ args: Vec<String>,
+ ) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
+ }
+ fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ResultEmpty)
+ }
+ fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id {
+ Self::ErrorSerdeJson => {
+ let value = unsafe {
+ any.downcast::<crate::errors::serde_json::ErrorSerdeJson>()
+ .unwrap_unchecked()
+ };
+ <crate::errors::serde_json::__internal_renderer_render_error_serde_json as ::mingling::Renderer>::render(
+ value,
+ )
+ }
+ Self::Metadata => {
+ let value = unsafe {
+ any.downcast::<cargo_metadata::Metadata>().unwrap_unchecked()
+ };
+ <crate::metadata::__internal_renderer_render_metadata as ::mingling::Renderer>::render(
+ value,
+ )
+ }
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::mingling::ChainProcess<Self::Enum> {
+ match any.member_id {
+ Self::EntryMetadata => {
+ let value = unsafe {
+ any.downcast::<crate::metadata::cmd_metadata::EntryMetadata>()
+ .unwrap_unchecked()
+ };
+ <crate::metadata::cmd_metadata::__internal_chain_handle_metadata as ::mingling::Chain<
+ Self::Enum,
+ >>::proc(value)
+ }
+ Self::EntryMinglingLinter => {
+ let value = unsafe {
+ any.downcast::<crate::linter::cmd_mlint::EntryMinglingLinter>()
+ .unwrap_unchecked()
+ };
+ <crate::linter::cmd_mlint::__internal_chain_handle_mlint as ::mingling::Chain<
+ Self::Enum,
+ >>::proc(value)
+ }
+ _ => {
+ ::core::panicking::panic_fmt(
+ format_args!("No chain found for type id: {0:?}", any.type_id),
+ );
+ }
+ }
+ }
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ #[allow(unused_imports)]
+ use crate::metadata::cmd_metadata::__internal_chain_handle_metadata;
+ use crate::metadata::cmd_metadata::ResultMetadata;
+ use crate::linter::cmd_mlint::__internal_chain_handle_mlint;
+ use crate::metadata::__internal_renderer_render_metadata;
+ use crate::metadata::cmd_metadata::CMDMetadata;
+ use crate::metadata::setup::ResMetadata;
+ use crate::metadata::cmd_metadata::EntryMetadata;
+ use crate::linter::cmd_mlint::CMDMinglingLinter;
+ use crate::errors::serde_json::ErrorSerdeJson;
+ use cargo_metadata::Metadata;
+ use crate::errors::serde_json::__internal_renderer_render_error_serde_json;
+ use crate::linter::cmd_mlint::EntryMinglingLinter;
+ match any.member_id {
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ Self::ErrorSerdeJson => true,
+ Self::Metadata => true,
+ _ => false,
+ }
+ }
+ fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ Self::EntryMetadata => true,
+ Self::EntryMinglingLinter => true,
+ _ => false,
+ }
+ }
+ fn structural_render(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ setting: &::mingling::StructuralRendererSetting,
+ ) -> Result<
+ ::mingling::RenderResult,
+ ::mingling::error::StructuralRendererSerializeError,
+ > {
+ #[allow(unused_imports)]
+ use crate::metadata::cmd_metadata::__internal_chain_handle_metadata;
+ use crate::metadata::cmd_metadata::ResultMetadata;
+ use crate::linter::cmd_mlint::__internal_chain_handle_mlint;
+ use crate::metadata::__internal_renderer_render_metadata;
+ use crate::metadata::cmd_metadata::CMDMetadata;
+ use crate::metadata::setup::ResMetadata;
+ use crate::metadata::cmd_metadata::EntryMetadata;
+ use crate::linter::cmd_mlint::CMDMinglingLinter;
+ use crate::errors::serde_json::ErrorSerdeJson;
+ use cargo_metadata::Metadata;
+ use crate::errors::serde_json::__internal_renderer_render_error_serde_json;
+ use crate::linter::cmd_mlint::EntryMinglingLinter;
+ match any.member_id {
+ Self::Metadata => {
+ let raw = unsafe { any.restore::<Metadata>().unwrap_unchecked() };
+ let mut __renderer_inner_result = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(
+ &raw,
+ setting,
+ &mut __renderer_inner_result,
+ )?;
+ Ok(__renderer_inner_result)
+ }
+ _ => {
+ let mut r = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
+ Ok(r)
+ }
+ }
+ }
+}
+impl ThisProgram {
+ /// Creates a new `Program<#name>` instance with default configuration.
+ pub fn new() -> ::mingling::Program<ThisProgram> {
+ ::mingling::Program::new()
+ }
+ /// Returns a static reference to the global `Program<#name>` singleton.
+ pub fn this() -> &'static ::mingling::Program<ThisProgram> {
+ &::mingling::this::<ThisProgram>()
+ }
+}
diff --git a/mingling_cli/pre/lint_registry.rs b/mingling_cli/pre/lint_registry.rs
new file mode 100644
index 0000000..09d67f9
--- /dev/null
+++ b/mingling_cli/pre/lint_registry.rs
@@ -0,0 +1,192 @@
+use std::{fs, io::Error};
+
+use just_template::{Template, tmpl};
+
+/// Generate lint module registry file (src/lints/mod.rs)
+///
+/// Read all Rust source files in the src/lints/ directory (excluding mod.rs itself),
+/// automatically generate module declarations and pub use export statements, and write them to mod.rs.
+pub fn gen_mod_file() -> Result<(), Error> {
+ let root = std::env::current_dir()?;
+ let lints_dir = root.join("src").join("lints");
+ let tmpl_file = root.join("tmpls").join("lints.tmpl");
+ let mod_file = root.join("src").join("lints.rs");
+
+ let mut template = Template::from(fs::read_to_string(tmpl_file).unwrap());
+
+ // Collect all .rs file names (without extension), excluding mod
+ let mut entries: Vec<String> = fs::read_dir(&lints_dir)?
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_file())
+ .filter_map(|e| {
+ e.path()
+ .file_stem()
+ .and_then(|s| s.to_str())
+ .map(|s| s.to_string())
+ })
+ .filter(|name| name != "mod")
+ .collect();
+
+ entries.sort();
+
+ // Generate module declarations and re-export statements
+ for name in &entries {
+ tmpl!(template, impls {
+ mod_name = name
+ })
+ }
+
+ // Generate run_all_lints call arms
+ for name in &entries {
+ tmpl!(template, calls {
+ name = name
+ })
+ }
+
+ fs::write(&mod_file, template.to_string())?;
+
+ Ok(())
+}
+
+/// Generate lint metadata registry
+///
+/// Parses each `.rs` file in `src/lints/` (excluding mod.rs and _init.rs),
+/// extracts doc-comment metadata, and writes the result as JSON.
+pub fn gen_lint_registry() -> Result<(), Error> {
+ let root = std::env::current_dir()?;
+ let lints_dir = root.join("src").join("lints");
+
+ let mut lints: Vec<serde_json::Value> = Vec::new();
+
+ for entry in fs::read_dir(&lints_dir)? {
+ let entry = entry?;
+ let path = entry.path();
+ if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("rs") {
+ continue;
+ }
+ let stem = match path.file_stem().and_then(|s| s.to_str()) {
+ Some(s) => s.to_string(),
+ None => continue,
+ };
+ if stem == "mod" {
+ continue;
+ }
+
+ let content = fs::read_to_string(&path)?;
+ if let Some(meta) = parse_lint_file(&content, &stem) {
+ lints.push(meta);
+ }
+ }
+
+ lints.sort_by(|a, b| {
+ a["name"]
+ .as_str()
+ .unwrap_or("")
+ .cmp(b["name"].as_str().unwrap_or(""))
+ });
+
+ let json = serde_json::json!({ "lints": lints });
+ let out_path = root.join("registry.json");
+ fs::write(&out_path, serde_json::to_string_pretty(&json).unwrap())?;
+ Ok(())
+}
+
+/// Parse a single lint `.rs` file and return its metadata as JSON.
+fn parse_lint_file(content: &str, name: &str) -> Option<serde_json::Value> {
+ // Collect leading doc comment lines (//!)
+ let mut doc_lines: Vec<String> = Vec::new();
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("//!") {
+ doc_lines.push(rest.trim().to_string());
+ } else if !doc_lines.is_empty() {
+ break;
+ }
+ }
+
+ // Title: first non-empty doc line
+ let title = doc_lines
+ .iter()
+ .find(|l| !l.is_empty())
+ .cloned()
+ .unwrap_or_default();
+
+ // Summary: between ## Summary and ## Metadata
+ let mut summary = String::new();
+ let mut in_summary = false;
+ for line in &doc_lines {
+ if line.contains("## Summary") {
+ in_summary = true;
+ continue;
+ }
+ if line.contains("## Metadata") {
+ in_summary = false;
+ }
+ if in_summary && !line.starts_with("## ") {
+ if !summary.is_empty() {
+ summary.push('\n');
+ }
+ summary.push_str(line.trim());
+ }
+ }
+
+ // Metadata section
+ let mut author = String::new();
+ let mut default_level = String::from("warn");
+ let mut active_on = String::from("File");
+ let mut in_metadata = false;
+
+ for line in &doc_lines {
+ if line.contains("## Metadata") {
+ in_metadata = true;
+ continue;
+ }
+ if in_metadata {
+ if line.starts_with("## ") {
+ break;
+ }
+ if let Some(val) = line
+ .strip_prefix("Author:")
+ .or_else(|| line.strip_prefix("Author:"))
+ {
+ author = val.trim().trim_matches('`').to_string();
+ }
+ if let Some(val) = line
+ .strip_prefix("Default:")
+ .or_else(|| line.strip_prefix("Default:"))
+ {
+ let raw = val.trim().trim_matches('`');
+ if raw == "warn" || raw == "allow" || raw == "deny" {
+ default_level = raw.to_string();
+ }
+ }
+ }
+ }
+
+ // Extract active_on from function signature (last occurrence = actual fn)
+ if let Some(pos) = content.rfind("pub fn linter(") {
+ let after = &content[pos..];
+ if let Some(colon) = after.find(':') {
+ let type_part = &after[colon + 1..].trim();
+ let raw = type_part
+ .trim_start_matches("syn::")
+ .split([' ', ')', ',', '\n'])
+ .next()
+ .unwrap_or("File");
+ // Strip "Item" prefix: ItemFn → Fn, ItemStruct → Struct, etc.
+ active_on = raw.strip_prefix("Item").unwrap_or(raw).to_string();
+ }
+ }
+
+ let mut meta = serde_json::Map::new();
+ meta.insert("author".into(), serde_json::Value::String(author));
+ meta.insert("default".into(), serde_json::Value::String(default_level));
+ meta.insert("active_on".into(), serde_json::Value::String(active_on));
+
+ Some(serde_json::json!({
+ "name": name,
+ "title": title,
+ "summary": summary.trim(),
+ "metadata": meta,
+ }))
+}
diff --git a/mingling_cli/pre/mod.rs b/mingling_cli/pre/mod.rs
new file mode 100644
index 0000000..8eacef4
--- /dev/null
+++ b/mingling_cli/pre/mod.rs
@@ -0,0 +1,2 @@
+mod lint_registry;
+pub use lint_registry::*;
diff --git a/mingling_cli/src/diagnostic.rs b/mingling_cli/src/diagnostic.rs
new file mode 100644
index 0000000..527e879
--- /dev/null
+++ b/mingling_cli/src/diagnostic.rs
@@ -0,0 +1,99 @@
+use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING};
+use annotate_snippets::{AnnotationKind, Group, Renderer, Snippet};
+use cargo_metadata::diagnostic::Diagnostic;
+use mingling::macros::{buffer, group, r_println, renderer};
+
+group!(Diagnostic);
+
+#[renderer(buffer)]
+pub fn render_diagnostic(diagnostic: Diagnostic) {
+ let report = diagnostic_to_report(&diagnostic);
+ let renderer = Renderer::styled();
+ let rendered = renderer.render(&report);
+ r_println!("{rendered}");
+}
+
+fn cargo_level_to_annotate(
+ level: cargo_metadata::diagnostic::DiagnosticLevel,
+) -> &'static annotate_snippets::Level<'static> {
+ use cargo_metadata::diagnostic::DiagnosticLevel;
+ match level {
+ DiagnosticLevel::Ice | DiagnosticLevel::Error => &ERROR,
+ DiagnosticLevel::Warning => &WARNING,
+ DiagnosticLevel::Note | DiagnosticLevel::FailureNote => &NOTE,
+ DiagnosticLevel::Help => &HELP,
+ _ => &ERROR,
+ }
+}
+
+/// 把 1-based char offset 转成 0-based byte offset
+fn char_offset_to_byte_offset(s: &str, char_offset: usize) -> usize {
+ s.char_indices()
+ .nth(char_offset.saturating_sub(1))
+ .map(|(i, _)| i)
+ .unwrap_or(s.len())
+}
+
+fn diagnostic_to_report<'a>(d: &'a Diagnostic) -> Vec<Group<'a>> {
+ let level = cargo_level_to_annotate(d.level);
+ let mut title = level.clone().primary_title(d.message.as_str());
+ if let Some(ref code) = d.code {
+ title = title.id(code.code.as_str());
+ }
+
+ let mut group = Group::with_title(title);
+
+ for span in &d.spans {
+ if !span.is_primary {
+ continue;
+ }
+
+ let source: String = span
+ .text
+ .iter()
+ .map(|l| l.text.as_str())
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let byte_range = if span.text.len() == 1 {
+ let line = &span.text[0];
+ let start = char_offset_to_byte_offset(&line.text, line.highlight_start);
+ let end = char_offset_to_byte_offset(&line.text, line.highlight_end);
+ start..end
+ } else {
+ let first = &span.text[0];
+ let last = span.text.last().unwrap();
+ let start = char_offset_to_byte_offset(&first.text, first.highlight_start);
+ let prefix_len: usize = span.text[..span.text.len() - 1]
+ .iter()
+ .map(|l| l.text.len() + 1)
+ .sum();
+ let end = prefix_len + char_offset_to_byte_offset(&last.text, last.highlight_end);
+ start..end
+ };
+
+ let mut snippet = Snippet::source(source)
+ .line_start(span.line_start)
+ .path(span.file_name.as_str());
+
+ if let Some(ref label) = span.label {
+ let annotation = AnnotationKind::Primary
+ .span(byte_range)
+ .label(label.as_str());
+ snippet = snippet.annotation(annotation);
+ } else {
+ let annotation = AnnotationKind::Primary.span(byte_range);
+ snippet = snippet.annotation(annotation);
+ }
+
+ group = group.element(snippet);
+ }
+
+ for child in &d.children {
+ let msg_level = cargo_level_to_annotate(child.level);
+ let msg = msg_level.clone().message(child.message.as_str());
+ group = group.element(msg);
+ }
+
+ vec![group]
+}
diff --git a/mingling_cli/src/errors.rs b/mingling_cli/src/errors.rs
new file mode 100644
index 0000000..cef9616
--- /dev/null
+++ b/mingling_cli/src/errors.rs
@@ -0,0 +1 @@
+pub mod serde_json;
diff --git a/mingling_cli/src/errors/serde_json.rs b/mingling_cli/src/errors/serde_json.rs
new file mode 100644
index 0000000..c73329c
--- /dev/null
+++ b/mingling_cli/src/errors/serde_json.rs
@@ -0,0 +1,8 @@
+use mingling::macros::{buffer, group, r_println, renderer};
+
+group!(ErrorSerdeJson = serde_json::Error);
+
+#[renderer(buffer)]
+pub fn render_error_serde_json(_err: ErrorSerdeJson) {
+ r_println!("serde");
+}
diff --git a/mingling_cli/src/linter.rs b/mingling_cli/src/linter.rs
index 870efeb..b3b2e7b 100644
--- a/mingling_cli/src/linter.rs
+++ b/mingling_cli/src/linter.rs
@@ -3,6 +3,8 @@ use mingling::{Program, macros::program_setup};
use crate::linter::cmd_mlint::CMDMinglingLinter;
pub mod cmd_mlint;
+pub mod mlint_attr;
+pub mod mlint_report;
#[program_setup]
pub fn mingling_linter_setup(program: &mut Program<crate::ThisProgram>) {
diff --git a/mingling_cli/src/linter/cmd_mlint.rs b/mingling_cli/src/linter/cmd_mlint.rs
index 1431c43..90871af 100644
--- a/mingling_cli/src/linter/cmd_mlint.rs
+++ b/mingling_cli/src/linter/cmd_mlint.rs
@@ -1,6 +1,119 @@
-use mingling::macros::{chain, dispatcher};
+use crate::linter::mlint_report::{MlintReport, StateLintReports};
+use cargo_metadata::Metadata;
+use mingling::LazyRes;
+use mingling::consts::REMAINS;
+use mingling::macros::{arg, chain, dispatcher, pack};
+use mingling::picker::EntryPicker;
-dispatcher!("mlint", CMDMinglingLinter => EntryMinglingLinter);
+dispatcher!("lint", CMDMinglingLinter => EntryMinglingLinter);
+
+/// Main linting function that processes all packages in the metadata.
+///
+/// Iterates through all packages and their targets (e.g., binaries, libraries, tests),
+/// reads Rust source files (`.rs`), parses them into ASTs, runs lint checks,
+/// and enriches each report with metadata information.
+async fn linter_main(metadata: &Metadata) -> Vec<MlintReport> {
+ // Vector to accumulate all lint reports
+ let mut all_reports = Vec::new();
+
+ // Iterate over all packages in the metadata
+ for package in &metadata.packages {
+ // Iterate over all targets within a package (e.g., bin, lib, test, etc.)
+ for target in &package.targets {
+ let path = &target.src_path;
+ // Only process Rust source files (with `.rs` extension)
+ if !path.as_str().ends_with(".rs") {
+ continue;
+ }
+ // Read the source file content
+ let source = match std::fs::read_to_string(path.as_str()) {
+ Ok(s) => s,
+ Err(_) => continue,
+ };
+ // Parse the source file into an AST (Abstract Syntax Tree)
+ let ast = match syn::parse_file(&source) {
+ Ok(f) => f,
+ Err(_) => continue,
+ };
+
+ // Run all lint checks and collect reports
+ let reports = crate::lints::run_all_lints(&ast, &source);
+ //
+ for mut r in reports {
+ // Enrich report with metadata information
+ r.file_name = path.as_str().to_string();
+ r.source_code = source.clone();
+ r.package_id = Some(package.id.to_string());
+ r.target_name = Some(target.name.clone());
+ r.target_kind = target.kind.first().map(|k| k.to_string());
+ r.target_src_path = Some(path.as_str().to_string());
+ all_reports.push(r);
+ }
+ }
+ }
+
+ all_reports
+}
+
+pack!(StateBeginLinter = ());
+
+#[chain]
+pub fn handle_lint(args: EntryMinglingLinter) -> StateBeginLinter {
+ let (with_checker, checker_args) = args
+ .pick_or(&arg![with_checker: Option<String>], || {
+ Some("cargo,check".to_string())
+ })
+ .pick(&REMAINS)
+ .unwrap();
+
+ // If with_checker is not set, proceed directly to the mingling lint phase
+ let Some(with_checker) = with_checker else {
+ return StateBeginLinter::new(());
+ };
+
+ let with_checker: Vec<&str> = with_checker.split(',').collect();
+ let checker_args: Vec<String> = checker_args.into();
+
+ // Run the outer checker (e.g. cargo check) with output passed through directly
+ execute_checker(&with_checker, checker_args.as_slice());
+
+ StateBeginLinter::new(())
+}
+
+/// Run the outer checker (e.g. cargo check) with output passed through directly.
+fn execute_checker(with_checker: &[&str], checker_args: &[String]) {
+ if with_checker.is_empty() {
+ return;
+ }
+
+ let checker_str = with_checker.join(" ");
+ let args_str = checker_args.join(" ");
+ let full_cmd = if args_str.is_empty() {
+ checker_str
+ } else {
+ format!("{} {}", checker_str, args_str)
+ };
+
+ let mut cmd = if cfg!(target_os = "windows") {
+ let mut c = std::process::Command::new("cmd");
+ c.args(["/C", &full_cmd]);
+ c
+ } else {
+ let mut c = std::process::Command::new("sh");
+ c.args(["-c", &full_cmd]);
+ c
+ };
+
+ // Pass through stdin/stdout/stderr so the user sees everything
+ let _ = cmd.status();
+}
#[chain]
-pub fn handle_mlint(_args: EntryMinglingLinter) {}
+pub async fn handle_state_begin_linter(
+ _: StateBeginLinter,
+ metadata: &mut LazyRes<crate::metadata::setup::ResMetadata>,
+) -> StateLintReports {
+ let metadata = metadata.get_ref().data();
+ let reports = linter_main(metadata).await;
+ StateLintReports::new(reports)
+}
diff --git a/mingling_cli/src/linter/mlint_attr.rs b/mingling_cli/src/linter/mlint_attr.rs
new file mode 100644
index 0000000..24528d4
--- /dev/null
+++ b/mingling_cli/src/linter/mlint_attr.rs
@@ -0,0 +1,39 @@
+/// Result of checking `mlint(allow/warn/deny(...))` attributes.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum MlintLevelOverride {
+ Allow,
+ Warn,
+ Deny,
+}
+
+/// Parse `mlint(allow/warn/deny(lint_name))` from attributes.
+/// Returns `None` if the lint is not mentioned in any mlint attribute.
+pub fn get_mlint_override(attrs: &[syn::Attribute], lint_name: &str) -> Option<MlintLevelOverride> {
+ for attr in attrs {
+ if !attr.path().is_ident("mlint") {
+ continue;
+ }
+ let list = match attr.meta.require_list() {
+ Ok(l) => l,
+ Err(_) => continue,
+ };
+ // TokenStream::to_string() adds spaces between tokens,
+ // e.g. `allow(xxx)` → `allow ( xxx )`. Remove all spaces to compare.
+ let flat: String = list
+ .tokens
+ .to_string()
+ .chars()
+ .filter(|c| !c.is_whitespace())
+ .collect();
+ for (keyword, variant) in [
+ ("allow", MlintLevelOverride::Allow),
+ ("warn", MlintLevelOverride::Warn),
+ ("deny", MlintLevelOverride::Deny),
+ ] {
+ if flat.contains(&format!("{keyword}({lint_name})")) {
+ return Some(variant);
+ }
+ }
+ }
+ None
+}
diff --git a/mingling_cli/src/linter/mlint_report.rs b/mingling_cli/src/linter/mlint_report.rs
new file mode 100644
index 0000000..3c52828
--- /dev/null
+++ b/mingling_cli/src/linter/mlint_report.rs
@@ -0,0 +1,413 @@
+use cargo_metadata::diagnostic::{
+ DiagnosticCodeBuilder, DiagnosticLevel as CargoLevel, DiagnosticSpanBuilder,
+ DiagnosticSpanLineBuilder,
+};
+
+use cargo_metadata::{Message, PackageId};
+
+use annotate_snippets::level::{ERROR, HELP, NOTE, WARNING};
+use annotate_snippets::{AnnotationKind, Group, Renderer, Snippet};
+use mingling::macros::{buffer, chain, pack, r_append, r_eprintln, renderer};
+use mingling::{AnyOutput, ProgramCollect, Routable};
+
+use crate::Next;
+use crate::metadata::setup::ResUsingJson;
+
+/// Complete structure of a Lint report, containing inspection results and associated metadata.
+pub struct MlintReport {
+ /// Source file name
+ pub file_name: String,
+
+ /// Full source text of the file, used to extract line content and compute byte offsets
+ pub source_code: String,
+
+ /// Severity level of the report
+ pub level: MlintLevel,
+
+ /// Name of the Lint
+ pub lint_code: String,
+
+ /// Content of the report
+ pub message: String,
+
+ /// Source code locations
+ pub spans: Vec<LintSpan>,
+
+ /// Attached sub-reports for this report
+ pub attached_reports: Vec<MlintReport>,
+
+ /// Package ID that this report belongs to
+ pub package_id: Option<String>,
+
+ /// Compilation target name that this report belongs to
+ pub target_name: Option<String>,
+
+ /// Compilation target type that this report belongs to
+ pub target_kind: Option<String>,
+
+ /// Compilation target source file path that this report belongs to
+ pub target_src_path: Option<String>,
+}
+
+/// Report severity level, indicating the seriousness of the Lint result.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum MlintLevel {
+ Error,
+ Warning,
+ Note,
+ Help,
+}
+
+/// Source code location span, representing a range of source code and its associated text information.
+pub struct LintSpan {
+ /// Starting line number (1-based)
+ pub line_start: usize,
+ /// Ending line number (1-based)
+ pub line_end: usize,
+ /// Starting column (1-based char offset)
+ pub column_start: usize,
+ /// Ending column (1-based char offset)
+ pub column_end: usize,
+ /// Source lines at this location
+ pub text: Vec<LintSpanLine>,
+ /// Optional label description
+ pub label: Option<String>,
+}
+
+/// A single line of text in a source location with highlight range.
+pub struct LintSpanLine {
+ /// Full source line text (no trailing `\n`)
+ pub text: String,
+ /// Highlight start (1-based char offset)
+ pub highlight_start: usize,
+ /// Highlight end (1-based char offset)
+ pub highlight_end: usize,
+}
+
+impl MlintReport {
+ /// Build a `LintSpan` from a syn spanned item and the source text.
+ pub fn span_from_syn<T: syn::spanned::Spanned>(value: &T, source: &str) -> LintSpan {
+ let span = value.span();
+ let start = span.start();
+ let end = span.end();
+
+ // Extract line content from source
+ let lines: Vec<&str> = source.lines().collect();
+ let text = if start.line == end.line && start.line <= lines.len() {
+ let line_text = lines[start.line.saturating_sub(1)];
+ let hl_start = proc_macro2_byte_col_to_char_1based(line_text, start.column);
+ let hl_end = proc_macro2_byte_col_to_char_1based(line_text, end.column);
+ vec![LintSpanLine {
+ text: line_text.to_string(),
+ highlight_start: hl_start,
+ highlight_end: hl_end,
+ }]
+ } else {
+ // Multi-line: generate line by line
+ (start.line..=end.line.min(lines.len()))
+ .map(|i| {
+ let line_text = lines[i.saturating_sub(1)];
+ let (hl_start, hl_end) = if i == start.line {
+ (
+ proc_macro2_byte_col_to_char_1based(line_text, start.column),
+ line_text.chars().count(),
+ )
+ } else if i == end.line {
+ (
+ 1,
+ proc_macro2_byte_col_to_char_1based(line_text, end.column),
+ )
+ } else {
+ (1, line_text.chars().count())
+ };
+ LintSpanLine {
+ text: line_text.to_string(),
+ highlight_start: hl_start,
+ highlight_end: hl_end,
+ }
+ })
+ .collect::<Vec<_>>()
+ };
+
+ LintSpan {
+ line_start: start.line,
+ line_end: end.line,
+ column_start: proc_macro2_byte_col_to_char_1based(
+ lines.get(start.line.saturating_sub(1)).unwrap_or(&""),
+ start.column,
+ ),
+ column_end: proc_macro2_byte_col_to_char_1based(
+ lines.get(end.line.saturating_sub(1)).unwrap_or(&""),
+ end.column,
+ ),
+ text,
+ label: None,
+ }
+ }
+
+ /// Compute byte offset from (line, column) within source.
+ /// line: 1-based, column: 1-based char offset.
+ pub fn line_col_to_byte_offset(&self, line: usize, col: usize) -> usize {
+ let mut byte_pos = 0usize;
+ for (i, line_str) in self.source_code.lines().enumerate() {
+ if i + 1 == line {
+ return byte_pos + char_1based_to_byte_offset(line_str, col);
+ }
+ byte_pos += line_str.len() + 1; // +1 for \n
+ }
+ self.source_code.len()
+ }
+}
+
+/// proc-macro2's LineColumn.column is **0-based byte offset**.
+/// Convert to 1-based char offset.
+fn proc_macro2_byte_col_to_char_1based(line: &str, byte_col: usize) -> usize {
+ line.char_indices()
+ .position(|(i, _)| i >= byte_col)
+ .map(|pos| pos + 1) // → 1-based
+ .unwrap_or(line.chars().count().max(1))
+}
+
+/// 1-based char offset → byte offset within a string
+fn char_1based_to_byte_offset(s: &str, char_1based: usize) -> usize {
+ s.char_indices()
+ .nth(char_1based.saturating_sub(1))
+ .map(|(i, _)| i)
+ .unwrap_or(s.len())
+}
+
+impl MlintReport {
+ pub fn to_annotate_snippet_render(&self) -> String {
+ let level = mlinit_level_to_annotate(&self.level);
+ let title = level.clone().primary_title(&self.message);
+ // code 不放在 title 里,改放在 note 中
+ let mut group = Group::with_title(title);
+
+ for span in &self.spans {
+ let source = span
+ .text
+ .iter()
+ .map(|l| l.text.as_str())
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let byte_range = if span.text.len() == 1 {
+ let line = &span.text[0];
+ let start = char_1based_to_byte_offset(&line.text, line.highlight_start);
+ let end = char_1based_to_byte_offset(&line.text, line.highlight_end);
+ start..end
+ } else {
+ let first = &span.text[0];
+ let last = span.text.last().unwrap();
+ let start = char_1based_to_byte_offset(&first.text, first.highlight_start);
+ let prefix_len: usize = span.text[..span.text.len() - 1]
+ .iter()
+ .map(|l| l.text.len() + 1)
+ .sum();
+ let end = prefix_len + char_1based_to_byte_offset(&last.text, last.highlight_end);
+ start..end
+ };
+
+ let mut snippet = Snippet::source(source)
+ .line_start(span.line_start)
+ .path(&self.file_name);
+
+ let annotation = match &span.label {
+ Some(label) => AnnotationKind::Primary
+ .span(byte_range)
+ .label(label.as_str()),
+ None => AnnotationKind::Primary.span(byte_range),
+ };
+ snippet = snippet.annotation(annotation);
+ group = group.element(snippet);
+ }
+
+ for child in &self.attached_reports {
+ let child_level = mlinit_level_to_annotate(&child.level);
+ let msg = child_level.clone().message(&child.message);
+ group = group.element(msg);
+ }
+
+ // 添加 note: `#[mlint(level([code]))]` on by default
+ if !self.lint_code.is_empty() {
+ let level_name = match self.level {
+ MlintLevel::Error => "deny",
+ MlintLevel::Warning => "warn",
+ MlintLevel::Note | MlintLevel::Help => "allow",
+ };
+ let note_text = format!(
+ "`#[mlint({level_name}([{}]))]` on by default",
+ self.lint_code
+ );
+ let note_msg = NOTE.clone().message(note_text);
+ group = group.element(note_msg);
+ }
+
+ let renderer = Renderer::styled();
+ renderer.render(&[group])
+ }
+}
+
+fn mlinit_level_to_annotate(level: &MlintLevel) -> &'static annotate_snippets::Level<'static> {
+ match level {
+ MlintLevel::Error => &ERROR,
+ MlintLevel::Warning => &WARNING,
+ MlintLevel::Note => &NOTE,
+ MlintLevel::Help => &HELP,
+ }
+}
+
+impl MlintReport {
+ pub fn to_compiler_message(&self) -> Message {
+ use cargo_metadata::{CompilerMessageBuilder, Edition, TargetBuilder};
+
+ let target_kind_str = self.target_kind.as_deref().unwrap_or("bin");
+ let target_kind_parsed: cargo_metadata::TargetKind = target_kind_str.into();
+ let crate_kind: cargo_metadata::CrateType = target_kind_str.into();
+
+ let target = TargetBuilder::default()
+ .name(self.target_name.as_deref().unwrap_or_default())
+ .kind(vec![target_kind_parsed])
+ .crate_types(vec![crate_kind])
+ .required_features(Vec::<String>::new())
+ .src_path(self.target_src_path.as_deref().unwrap_or_default())
+ .edition(Edition::E2021)
+ .doctest(false)
+ .test(false)
+ .doc(false)
+ .build()
+ .unwrap();
+
+ let diagnostic = self.build_diagnostic(
+ &self.message,
+ &self.lint_code,
+ &self.level,
+ &self.spans,
+ &self.attached_reports,
+ );
+
+ Message::CompilerMessage(
+ CompilerMessageBuilder::default()
+ .package_id(PackageId {
+ repr: self.package_id.clone().unwrap_or_else(|| "unknown".into()),
+ })
+ .target(target)
+ .message(diagnostic)
+ .build()
+ .unwrap(),
+ )
+ }
+
+ fn build_diagnostic(
+ &self,
+ message: &str,
+ code: &str,
+ level: &MlintLevel,
+ spans: &[LintSpan],
+ children: &[MlintReport],
+ ) -> cargo_metadata::diagnostic::Diagnostic {
+ cargo_metadata::diagnostic::DiagnosticBuilder::default()
+ .message(message)
+ .code(
+ DiagnosticCodeBuilder::default()
+ .code(code)
+ .explanation(None)
+ .build()
+ .unwrap(),
+ )
+ .level(match level {
+ MlintLevel::Error => CargoLevel::Error,
+ MlintLevel::Warning => CargoLevel::Warning,
+ MlintLevel::Note => CargoLevel::Note,
+ MlintLevel::Help => CargoLevel::Help,
+ })
+ .spans(
+ spans
+ .iter()
+ .map(|s| self.lint_span_to_diagnostic_span(s))
+ .collect::<Vec<_>>(),
+ )
+ .children(
+ children
+ .iter()
+ .map(|c| {
+ self.build_diagnostic(
+ &c.message,
+ &c.lint_code,
+ &c.level,
+ &c.spans,
+ &c.attached_reports,
+ )
+ })
+ .collect::<Vec<_>>(),
+ )
+ .rendered(None)
+ .build()
+ .unwrap()
+ }
+
+ fn lint_span_to_diagnostic_span(
+ &self,
+ span: &LintSpan,
+ ) -> cargo_metadata::diagnostic::DiagnosticSpan {
+ let byte_start = self.line_col_to_byte_offset(span.line_start, span.column_start);
+ let byte_end = self.line_col_to_byte_offset(span.line_end, span.column_end);
+
+ DiagnosticSpanBuilder::default()
+ .file_name(&self.file_name)
+ .byte_start(byte_start as u32)
+ .byte_end(byte_end as u32)
+ .line_start(span.line_start)
+ .line_end(span.line_end)
+ .column_start(span.column_start)
+ .column_end(span.column_end)
+ .is_primary(true)
+ .text(
+ span.text
+ .iter()
+ .map(|l| {
+ DiagnosticSpanLineBuilder::default()
+ .text(&l.text)
+ .highlight_start(l.highlight_start)
+ .highlight_end(l.highlight_end)
+ .build()
+ .unwrap()
+ })
+ .collect::<Vec<_>>(),
+ )
+ .label(span.label.clone())
+ .suggested_replacement(None::<String>)
+ .suggestion_applicability(None::<cargo_metadata::diagnostic::Applicability>)
+ .expansion(None)
+ .build()
+ .unwrap()
+ }
+}
+
+pack!(StateLintReports = Vec<MlintReport>);
+pack!(ResultLintReportsAnnotateSnippet = Vec<MlintReport>);
+pack!(ResultLintReportsJson = Vec<MlintReport>);
+
+#[chain]
+pub fn handle_state_lint_reports(reports: StateLintReports, using_json: &ResUsingJson) -> Next {
+ if using_json.using {
+ ResultLintReportsJson::new(reports.inner).to_render()
+ } else {
+ ResultLintReportsAnnotateSnippet::new(reports.inner).to_render()
+ }
+}
+
+#[renderer(buffer)]
+pub fn render_lint_reports(reports: ResultLintReportsAnnotateSnippet) {
+ for report in reports.inner {
+ r_eprintln!("{}", report.to_annotate_snippet_render());
+ }
+}
+
+#[renderer(buffer)]
+pub fn render_lint_reports_json(reports: ResultLintReportsJson) {
+ for report in reports.inner {
+ // DIRTY: Dispatch to the Message renderer using AnyOutput to obtain the render result and append it to the Buffer
+ r_append!(|| { crate::ThisProgram::render(AnyOutput::new(report.to_compiler_message())) });
+ }
+}
diff --git a/mingling_cli/src/lints.rs b/mingling_cli/src/lints.rs
new file mode 100644
index 0000000..5bed483
--- /dev/null
+++ b/mingling_cli/src/lints.rs
@@ -0,0 +1,61 @@
+#![allow(unused)]
+use crate::linter::mlint_report::{MlintLevel, MlintReport};
+
+mod template_linter;
+pub use template_linter::linter as template_linter;
+mod unnecessary_render_result_creation;
+pub use unnecessary_render_result_creation::linter as unnecessary_render_result_creation;
+
+/// Run all registered lints on a parsed file with its source text.
+pub fn run_all_lints(file: &syn::File, source: &str) -> Vec<MlintReport> {
+ use crate::linter::mlint_attr::{get_mlint_override, MlintLevelOverride};
+ let mut reports = vec![];
+ for item in &file.items {
+ if let syn::Item::Fn(f) = item {
+ // Check item-level #[mlint(allow/warn/deny(template_linter))]
+ let skip = get_mlint_override(&f.attrs, "template_linter") == Some(MlintLevelOverride::Allow);
+ if !skip {
+ let mut rs = template_linter::linter(f.clone(), source);
+ // Apply deny override at item level
+ if get_mlint_override(&f.attrs, "template_linter") == Some(MlintLevelOverride::Deny) {
+ for r in &mut rs { r.level = MlintLevel::Error; }
+ }
+ reports.extend(rs);
+ }
+ // Check item-level #[mlint(allow/warn/deny(unnecessary_render_result_creation))]
+ let skip = get_mlint_override(&f.attrs, "unnecessary_render_result_creation") == Some(MlintLevelOverride::Allow);
+ if !skip {
+ let mut rs = unnecessary_render_result_creation::linter(f.clone(), source);
+ // Apply deny override at item level
+ if get_mlint_override(&f.attrs, "unnecessary_render_result_creation") == Some(MlintLevelOverride::Deny) {
+ for r in &mut rs { r.level = MlintLevel::Error; }
+ }
+ reports.extend(rs);
+ }
+
+ }
+ }
+ // Apply file-level #![mlint(allow/warn/deny(...))] overrides
+ for r in &mut reports {
+ let name = &r.lint_code;
+ if let Some(override_level) = get_mlint_override(&file.attrs, name) {
+ match override_level {
+ MlintLevelOverride::Allow => {
+ // remove by marking; filtered below
+ r.level = MlintLevel::Help; // sentinel: filtered out
+ }
+ MlintLevelOverride::Deny => {
+ r.level = MlintLevel::Error;
+ }
+ MlintLevelOverride::Warn => {
+ if r.level != MlintLevel::Error {
+ r.level = MlintLevel::Warning;
+ }
+ }
+ }
+ }
+ }
+ // Remove allowed reports (sentinel = Help)
+ reports.retain(|r| r.level != MlintLevel::Help);
+ reports
+} \ No newline at end of file
diff --git a/mingling_cli/src/lints/template_linter.rs b/mingling_cli/src/lints/template_linter.rs
new file mode 100644
index 0000000..4ea2f94
--- /dev/null
+++ b/mingling_cli/src/lints/template_linter.rs
@@ -0,0 +1,48 @@
+//! Template Linter
+//!
+//! ## Summary
+//!
+//! This is a template Linter that introduces how to add a Lint for Mingling.
+//! You can write an introduction for this Linter in the Summary section, for example:
+//!
+//! - Trigger conditions
+//! - Why is it necessary?
+//!
+//! ## Metadata
+//!
+//! > This section is the **Metadata** section, which needs to be filled in correctly.
+//! > These contents will eventually be compiled as the Linter's behavior.
+//!
+//! Author: `Weicao-CatilGrass`
+//! Default: `allow`
+// ^^^^^ Supported parameters: `warn`, `allow`, `deny`
+
+// --- ABOUT AUTO IDENTIFICATION RULES ---
+//
+// The compiler will treat code with the following structure as a Linter entry point:
+// |
+// --> your_linter_module.rs
+// |
+// | pub fn linter(ast: syn::ItemX) -> Vec<MlintReport> {
+// | /* ... */ ^^^^^^^^^^ Your linter scope
+// | }
+// |
+// = note: Please ensure your function is `pub`, named `linter`, and returns `Vec<MlintReport>`
+//
+// --- ABOUT AUTO IDENTIFICATION RULES ---
+
+use crate::linter::mlint_report::MlintReport;
+
+pub fn linter(_ast: syn::ItemFn, _source: &str) -> Vec<MlintReport> {
+ // ^^^^^^^^^^^ Supported parameters:
+ // | syn::File
+ // | syn::ItemImpl
+ // | syn::ItemStruct
+ // | syn::ItemEnum
+ // | syn::ItemTrait
+ // | syn::ItemFn
+ // | syn::ItemMacro
+ // | syn::ItemMod
+ // | syn::ItemUnion
+ vec![]
+}
diff --git a/mingling_cli/src/lints/unnecessary_render_result_creation.rs b/mingling_cli/src/lints/unnecessary_render_result_creation.rs
new file mode 100644
index 0000000..fa6809b
--- /dev/null
+++ b/mingling_cli/src/lints/unnecessary_render_result_creation.rs
@@ -0,0 +1,157 @@
+//! Unnecessary Manual RenderResult Creation
+//!
+//! ## Summary
+//!
+//! Detects `#[renderer]` functions that manually create a `RenderResult` and
+//! manage it via `r_println!(r, ...)` style calls, when they could be simplified
+//! to `#[renderer(buffer)]` which handles the buffer automatically.
+//!
+//! This lint will not trigger if `r` is used outside of `r_println!`,
+//! `r_eprintln!`, `r_print`, `r_eprint`, or `r_append`.
+//!
+//! ## Metadata
+//!
+//! Author: `Weicao-CatilGrass`
+//! Default: `warn`
+
+use crate::linter::mlint_report::{MlintLevel, MlintReport};
+
+pub fn linter(ast: syn::ItemFn, source: &str) -> Vec<MlintReport> {
+ if !has_renderer_attr(&ast) {
+ return vec![];
+ }
+
+ let stmts = &ast.block.stmts;
+ if stmts.len() < 2 {
+ return vec![];
+ }
+
+ let (r_ident, let_idx) = match find_render_result_new(stmts) {
+ Some(pair) => pair,
+ None => return vec![],
+ };
+
+ let r_name = r_ident.to_string();
+ let mut only_print_and_return = true;
+ let mut print_count = 0;
+
+ for stmt in &stmts[let_idx + 1..] {
+ if !check_stmt_usage(stmt, &r_name, &mut print_count) {
+ only_print_and_return = false;
+ break;
+ }
+ }
+
+ if only_print_and_return && print_count > 0 {
+ let span = MlintReport::span_from_syn(&ast.sig, source);
+ vec![MlintReport {
+ file_name: String::new(),
+ source_code: source.to_string(),
+ level: MlintLevel::Warning,
+ lint_code: "unnecessary_render_result_creation".into(),
+ message: format!(
+ "unnecessary manual `RenderResult` creation in `{}`: use `#[renderer(buffer)]` instead",
+ ast.sig.ident,
+ ),
+ spans: vec![span],
+ attached_reports: vec![MlintReport {
+ file_name: String::new(),
+ source_code: String::new(),
+ level: MlintLevel::Help,
+ lint_code: String::new(),
+ message: "change to `#[renderer(buffer)]` and use `r_println!(...)` without the `r` parameter".into(),
+ spans: vec![],
+ attached_reports: vec![],
+ package_id: None,
+ target_name: None,
+ target_kind: None,
+ target_src_path: None,
+ }],
+ package_id: None,
+ target_name: None,
+ target_kind: None,
+ target_src_path: None,
+ }]
+ } else {
+ vec![]
+ }
+}
+
+fn has_renderer_attr(func: &syn::ItemFn) -> bool {
+ func.attrs.iter().any(|a| {
+ if !a.path().is_ident("renderer") {
+ return false;
+ }
+ if let Ok(list) = a.meta.require_list() {
+ let tokens = list.tokens.to_string();
+ if tokens.contains("buffer") {
+ return false;
+ }
+ }
+ true
+ })
+}
+
+fn find_render_result_new(stmts: &[syn::Stmt]) -> Option<(proc_macro2::Ident, usize)> {
+ for (i, stmt) in stmts.iter().enumerate() {
+ if let syn::Stmt::Local(local) = stmt
+ && let Some(init) = &local.init
+ && let syn::Pat::Ident(pat_id) = &local.pat
+ && pat_id.mutability.is_some()
+ && let syn::Expr::Call(call) = &*init.expr
+ && let syn::Expr::Path(expr_path) = call.func.as_ref()
+ {
+ let segs = &expr_path.path.segments;
+ let matches = match segs.len() {
+ 2 => segs[0].ident == "RenderResult" && segs[1].ident == "new",
+ 3 => {
+ segs[0].ident == "mingling"
+ && segs[1].ident == "RenderResult"
+ && segs[2].ident == "new"
+ }
+ _ => false,
+ };
+ if matches {
+ return Some((pat_id.ident.clone(), i));
+ }
+ }
+ }
+ None
+}
+
+fn check_stmt_usage(stmt: &syn::Stmt, r_name: &str, print_count: &mut usize) -> bool {
+ // return r; → allowed
+ if let syn::Stmt::Expr(expr, _) = stmt
+ && let syn::Expr::Return(ret) = expr
+ && let Some(ret_expr) = &ret.expr
+ && let syn::Expr::Path(p) = ret_expr.as_ref()
+ && p.path.is_ident(r_name)
+ {
+ return true;
+ }
+
+ // r_println!(r, ...) → allowed
+ if let syn::Stmt::Macro(stmt_mac) = stmt {
+ let macro_name = stmt_mac
+ .mac
+ .path
+ .segments
+ .last()
+ .map(|s| s.ident.to_string())
+ .unwrap_or_default();
+ let is_r_macro = matches!(
+ macro_name.as_str(),
+ "r_println" | "r_eprintln" | "r_print" | "r_eprint" | "r_append"
+ );
+ if is_r_macro
+ && let Some(first_arg) = stmt_mac.mac.tokens.clone().into_iter().next()
+ && first_arg.to_string() == *r_name
+ {
+ *print_count += 1;
+ return true;
+ }
+ }
+
+ let s = format!("{stmt:#?}");
+ !s.contains(&format!("\"{r_name}\""))
+}
diff --git a/mingling_cli/src/main.rs b/mingling_cli/src/main.rs
index 59fa92d..c822a12 100644
--- a/mingling_cli/src/main.rs
+++ b/mingling_cli/src/main.rs
@@ -1,28 +1,29 @@
use crate::{linter::MinglingLinterSetup, metadata::MinglingMetadataSetup};
use mingling::{
macros::gen_program,
- setup::{
- ExitCodeSetup,
- picker::{HelpFlagSetup, StructuralRendererSetup},
- },
+ setup::{ExitCodeSetup, picker::HelpFlagSetup},
};
+pub mod diagnostic;
+pub mod errors;
pub mod linter;
+pub mod lints;
+pub mod message;
pub mod metadata;
-fn main() {
+#[tokio::main]
+async fn main() {
let mut program = ThisProgram::new();
// Setups
program.with_setup(HelpFlagSetup::default());
program.with_setup(ExitCodeSetup::default());
- program.with_setup(StructuralRendererSetup);
program.with_setup(MinglingMetadataSetup);
program.with_setup(MinglingLinterSetup);
// Exec
- program.exec_and_exit();
+ program.exec_and_exit().await;
}
gen_program!();
diff --git a/mingling_cli/src/message.rs b/mingling_cli/src/message.rs
new file mode 100644
index 0000000..3e50c30
--- /dev/null
+++ b/mingling_cli/src/message.rs
@@ -0,0 +1,10 @@
+use cargo_metadata::Message;
+use mingling::macros::{buffer, group, r_println, renderer, renderify};
+
+group!(Message);
+
+#[renderer(buffer, renderify)]
+pub fn render_message(msg: Message) {
+ let r = serde_json::to_string(&msg)?;
+ r_println!("{}", r);
+}
diff --git a/mingling_cli/src/metadata.rs b/mingling_cli/src/metadata.rs
index 0c0b0bf..2e7aeef 100644
--- a/mingling_cli/src/metadata.rs
+++ b/mingling_cli/src/metadata.rs
@@ -1,4 +1,8 @@
-use mingling::{Program, macros::program_setup};
+use cargo_metadata::Metadata;
+use mingling::{
+ Program,
+ macros::{buffer, group, program_setup, r_println, renderer, renderify},
+};
use crate::metadata::{cmd_metadata::CMDMetadata, setup::CargoMetadataSetup};
@@ -10,3 +14,11 @@ pub fn mingling_metadata_setup(program: &mut Program<crate::ThisProgram>) {
program.with_setup(CargoMetadataSetup);
program.with_dispatcher(CMDMetadata);
}
+
+group!(Metadata);
+
+#[renderer(buffer, renderify)]
+pub fn render_metadata(metadata: Metadata) {
+ let result = serde_json::to_string(&metadata)?;
+ r_println!("{}", result);
+}
diff --git a/mingling_cli/src/metadata/cmd_metadata.rs b/mingling_cli/src/metadata/cmd_metadata.rs
index 867e606..18241a2 100644
--- a/mingling_cli/src/metadata/cmd_metadata.rs
+++ b/mingling_cli/src/metadata/cmd_metadata.rs
@@ -1,16 +1,17 @@
+use cargo_metadata::Metadata;
use mingling::{
LazyRes,
- macros::{chain, dispatcher, pack_structural},
+ macros::{chain, dispatcher, pack},
};
use crate::metadata::setup::ResMetadata;
dispatcher!("metadata");
-pack_structural!(ResultMetadata = ResMetadata);
+pack!(ResultMetadata = ResMetadata);
#[chain]
-pub fn handle_metadata(_: EntryMetadata, metadata: &mut LazyRes<ResMetadata>) -> ResultMetadata {
+pub fn handle_metadata(_: EntryMetadata, metadata: &mut LazyRes<ResMetadata>) -> Metadata {
let metadata = metadata.get_ref().clone();
- ResultMetadata::new(metadata)
+ metadata.data().clone()
}
diff --git a/mingling_cli/src/metadata/setup.rs b/mingling_cli/src/metadata/setup.rs
index 4a891b6..198baf1 100644
--- a/mingling_cli/src/metadata/setup.rs
+++ b/mingling_cli/src/metadata/setup.rs
@@ -3,7 +3,6 @@ use std::{env::current_dir, path::PathBuf};
use cargo_metadata::{CargoOpt, MetadataCommand};
use mingling::{
LazyInit, Program,
- consts::REMAINS,
macros::program_setup,
picker::{IntoPicker, value::Flag},
prelude::*,
@@ -16,11 +15,28 @@ pub const CARGO_TOML: &str = "Cargo.toml";
use cargo_metadata::Metadata;
use serde::Serialize;
+/// Resource holding parsed `cargo metadata` output.
+///
+/// This is lazily initialized during program setup by calling `cargo metadata`
+/// with the appropriate CLI flags (e.g., `--features`, `--all-features`,
+/// `--no-default-features`, `--no-deps`, `--message-format`).
+///
+/// Access the inner [`Metadata`] via [`ResMetadata::data()`], which panics if
+/// called before initialization (guaranteed not to happen in normal usage).
#[derive(Default, Clone, Serialize)]
pub struct ResMetadata {
data: Option<Metadata>,
}
+/// Resource indicating whether the output format is JSON.
+///
+/// Set to `true` when `--message-format json` (or similar) is passed.
+/// Used by renderers to decide whether to serialize structs as JSON.
+#[derive(Default, Clone, Serialize)]
+pub struct ResUsingJson {
+ pub using: bool,
+}
+
impl ResMetadata {
/// Returns a reference to the parsed `cargo metadata`.
///
@@ -38,7 +54,8 @@ impl ResMetadata {
#[program_setup]
pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) {
- let args = program.take_args();
+ let args = program.get_args().to_vec();
+
let (
feature_args,
manifest_path,
@@ -46,7 +63,6 @@ pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) {
enable_all_features,
no_default_features,
no_deps,
- args,
) = args
.pick(&arg![features: Vec<String>])
.pick(&arg![manifest_path: Option<String>])
@@ -54,12 +70,12 @@ pub fn cargo_metadata_setup(program: &mut Program<crate::ThisProgram>) {
.pick(&arg![all_features: Flag])
.pick(&arg![no_default_features: Flag])
.pick(&arg![no_deps: Flag])
- .pick(&REMAINS)
.unwrap();
- program.replace_args(args.into());
- // Bind format setting to StructuralRenderer
- program.structural_renderer_name = message_format.into();
+ // Is Using Json
+ program.with_resource(ResUsingJson {
+ using: message_format.contains("json"),
+ });
// Current Dir
let current_dir = current_dir().unwrap();
diff --git a/mingling_cli/tmpls/lints.tmpl b/mingling_cli/tmpls/lints.tmpl
new file mode 100644
index 0000000..7507f98
--- /dev/null
+++ b/mingling_cli/tmpls/lints.tmpl
@@ -0,0 +1,56 @@
+#![allow(unused)]
+use crate::linter::mlint_report::{MlintLevel, MlintReport};
+
+>>>>>>>>>> impls;
+
+/// Run all registered lints on a parsed file with its source text.
+pub fn run_all_lints(file: &syn::File, source: &str) -> Vec<MlintReport> {
+ use crate::linter::mlint_attr::{get_mlint_override, MlintLevelOverride};
+ let mut reports = vec![];
+ for item in &file.items {
+ if let syn::Item::Fn(f) = item {
+>>>>>>>>>> calls;
+
+@@@ >>> calls
+ // Check item-level #[mlint(allow/warn/deny(<<<name>>>))]
+ let skip = get_mlint_override(&f.attrs, "<<<name>>>") == Some(MlintLevelOverride::Allow);
+ if !skip {
+ let mut rs = <<<name>>>::linter(f.clone(), source);
+ // Apply deny override at item level
+ if get_mlint_override(&f.attrs, "<<<name>>>") == Some(MlintLevelOverride::Deny) {
+ for r in &mut rs { r.level = MlintLevel::Error; }
+ }
+ reports.extend(rs);
+ }
+@@@ <<<
+ }
+ }
+ // Apply file-level #![mlint(allow/warn/deny(...))] overrides
+ for r in &mut reports {
+ let name = &r.lint_code;
+ if let Some(override_level) = get_mlint_override(&file.attrs, name) {
+ match override_level {
+ MlintLevelOverride::Allow => {
+ // remove by marking; filtered below
+ r.level = MlintLevel::Help; // sentinel: filtered out
+ }
+ MlintLevelOverride::Deny => {
+ r.level = MlintLevel::Error;
+ }
+ MlintLevelOverride::Warn => {
+ if r.level != MlintLevel::Error {
+ r.level = MlintLevel::Warning;
+ }
+ }
+ }
+ }
+ }
+ // Remove allowed reports (sentinel = Help)
+ reports.retain(|r| r.level != MlintLevel::Help);
+ reports
+}
+
+@@@ >>> impls
+mod <<<mod_name>>>;
+pub use <<<mod_name>>>::linter as <<<mod_name>>>;
+@@@ <<<