@tishlang/tish 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/error.rs +2 -8
- package/crates/js_to_tish/src/transform/expr.rs +101 -130
- package/crates/js_to_tish/src/transform/stmt.rs +25 -22
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +76 -29
- package/crates/tish/src/main.rs +85 -54
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/integration_test.rs +197 -47
- package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
- package/crates/tish/tests/shortcircuit.rs +19 -4
- package/crates/tish_ast/src/ast.rs +12 -14
- package/crates/tish_build_utils/src/lib.rs +64 -6
- package/crates/tish_builtins/src/array.rs +52 -21
- package/crates/tish_builtins/src/construct.rs +2 -8
- package/crates/tish_builtins/src/globals.rs +30 -15
- package/crates/tish_builtins/src/lib.rs +5 -5
- package/crates/tish_builtins/src/math.rs +5 -3
- package/crates/tish_builtins/src/string.rs +71 -19
- package/crates/tish_bytecode/src/chunk.rs +0 -1
- package/crates/tish_bytecode/src/compiler.rs +164 -60
- package/crates/tish_bytecode/src/opcode.rs +13 -4
- package/crates/tish_bytecode/src/peephole.rs +2 -2
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +989 -318
- package/crates/tish_compile/src/infer.rs +69 -19
- package/crates/tish_compile/src/lib.rs +21 -8
- package/crates/tish_compile/src/resolve.rs +515 -94
- package/crates/tish_compile/src/types.rs +10 -14
- package/crates/tish_compile_js/src/codegen.rs +34 -13
- package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
- package/crates/tish_compiler_wasm/src/lib.rs +16 -13
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +40 -48
- package/crates/tish_core/src/json.rs +5 -3
- package/crates/tish_core/src/lib.rs +1 -1
- package/crates/tish_core/src/uri.rs +9 -6
- package/crates/tish_core/src/value.rs +92 -28
- package/crates/tish_cranelift/src/link.rs +6 -9
- package/crates/tish_cranelift/src/lower.rs +14 -8
- package/crates/tish_eval/src/eval.rs +398 -141
- package/crates/tish_eval/src/lib.rs +10 -6
- package/crates/tish_eval/src/natives.rs +95 -38
- package/crates/tish_eval/src/promise.rs +14 -8
- package/crates/tish_eval/src/timers.rs +28 -19
- package/crates/tish_eval/src/value.rs +10 -3
- package/crates/tish_fmt/src/lib.rs +29 -13
- package/crates/tish_lexer/src/lib.rs +217 -63
- package/crates/tish_lexer/src/token.rs +6 -6
- package/crates/tish_llvm/src/lib.rs +15 -8
- package/crates/tish_lsp/src/main.rs +41 -43
- package/crates/tish_native/src/build.rs +38 -15
- package/crates/tish_native/src/lib.rs +76 -32
- package/crates/tish_opt/src/lib.rs +67 -50
- package/crates/tish_parser/src/lib.rs +36 -11
- package/crates/tish_parser/src/parser.rs +172 -87
- package/crates/tish_runtime/src/http.rs +15 -6
- package/crates/tish_runtime/src/http_fetch.rs +24 -14
- package/crates/tish_runtime/src/lib.rs +224 -168
- package/crates/tish_runtime/src/promise.rs +1 -5
- package/crates/tish_runtime/src/ws.rs +45 -20
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
- package/crates/tish_ui/src/jsx.rs +41 -22
- package/crates/tish_ui/src/lib.rs +2 -2
- package/crates/tish_vm/src/vm.rs +320 -116
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
- package/crates/tish_wasm/src/lib.rs +38 -28
- package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -41,7 +41,10 @@ fn string_strict_eq_logical_or_inside_ternary_repl_last_expr() {
|
|
|
41
41
|
let opt = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
|
|
42
42
|
let v_peep = run_chunk(&compile_for_repl(&opt).expect("compile repl"));
|
|
43
43
|
let v_unopt = run_chunk(&compile_for_repl_unoptimized(&opt).expect("compile repl unopt"));
|
|
44
|
-
assert!(
|
|
44
|
+
assert!(
|
|
45
|
+
v_peep.strict_eq(&v_unopt),
|
|
46
|
+
"peep={v_peep:?} unopt={v_unopt:?}"
|
|
47
|
+
);
|
|
45
48
|
assert!(
|
|
46
49
|
matches!(&v_peep, Value::Number(n) if *n == 1.0),
|
|
47
50
|
"expected 1, got {v_peep:?}"
|
|
@@ -62,7 +65,8 @@ fn logical_or_strict_eq_peephole_matches_unoptimized() {
|
|
|
62
65
|
);
|
|
63
66
|
|
|
64
67
|
let v_peep_repl = run_chunk(&compile_for_repl(&program).expect("compile repl"));
|
|
65
|
-
let v_raw_repl =
|
|
68
|
+
let v_raw_repl =
|
|
69
|
+
run_chunk(&compile_for_repl_unoptimized(&program).expect("compile repl unopt"));
|
|
66
70
|
assert!(
|
|
67
71
|
v_peep_repl.strict_eq(&v_raw_repl),
|
|
68
72
|
"repl: peep={v_peep_repl:?} raw={v_raw_repl:?}"
|
|
@@ -111,7 +115,8 @@ fn string_strict_eq_logical_or_peephole_matches_unoptimized() {
|
|
|
111
115
|
/// `tish run path/to/file.tish` uses merge_modules; ensure that matches plain parse for the fixture.
|
|
112
116
|
#[test]
|
|
113
117
|
fn merged_module_program_bytecode_matches_parse_for_string_or_fixture() {
|
|
114
|
-
let fixture =
|
|
118
|
+
let fixture =
|
|
119
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/or_string_cmd.tish");
|
|
115
120
|
let src = std::fs::read_to_string(&fixture).expect("read fixture");
|
|
116
121
|
let modules = tishlang_compile::resolve_project(&fixture, Some(fixture.parent().unwrap()))
|
|
117
122
|
.expect("resolve");
|
|
@@ -107,27 +107,33 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
|
|
|
107
107
|
std::fs::create_dir_all(&out_dir_abs).map_err(|e| WasmError {
|
|
108
108
|
message: format!("Cannot create output directory: {}", e),
|
|
109
109
|
})?;
|
|
110
|
-
let workspace_root =
|
|
111
|
-
message: e
|
|
112
|
-
})?;
|
|
110
|
+
let workspace_root =
|
|
111
|
+
tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
|
|
113
112
|
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
|
|
114
113
|
let build_status = Command::new(&cargo)
|
|
115
114
|
.current_dir(&workspace_root)
|
|
116
115
|
.args([
|
|
117
|
-
"build",
|
|
118
|
-
"
|
|
119
|
-
"
|
|
116
|
+
"build",
|
|
117
|
+
"-p",
|
|
118
|
+
"tishlang_wasm_runtime",
|
|
119
|
+
"--target",
|
|
120
|
+
"wasm32-unknown-unknown",
|
|
121
|
+
"--release",
|
|
122
|
+
"--features",
|
|
123
|
+
"browser",
|
|
120
124
|
])
|
|
121
125
|
.status()
|
|
122
|
-
.map_err(|e| WasmError {
|
|
126
|
+
.map_err(|e| WasmError {
|
|
127
|
+
message: format!("Failed to run cargo: {}", e),
|
|
128
|
+
})?;
|
|
123
129
|
if !build_status.success() {
|
|
124
130
|
return Err(WasmError {
|
|
125
131
|
message: "Failed to build wasm runtime. Run: rustup target add wasm32-unknown-unknown"
|
|
126
132
|
.to_string(),
|
|
127
133
|
});
|
|
128
134
|
}
|
|
129
|
-
let wasm_artifact =
|
|
130
|
-
.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
|
|
135
|
+
let wasm_artifact =
|
|
136
|
+
workspace_root.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
|
|
131
137
|
if !wasm_artifact.exists() {
|
|
132
138
|
return Err(WasmError {
|
|
133
139
|
message: format!("Wasm artifact not found: {}", wasm_artifact.display()),
|
|
@@ -137,17 +143,25 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
|
|
|
137
143
|
let out_name = stem.to_string();
|
|
138
144
|
let bindgen_status = Command::new(&wasm_bindgen)
|
|
139
145
|
.args([
|
|
140
|
-
"--target",
|
|
141
|
-
"
|
|
142
|
-
"--out-
|
|
146
|
+
"--target",
|
|
147
|
+
"web",
|
|
148
|
+
"--out-dir",
|
|
149
|
+
out_dir_abs.to_str().unwrap(),
|
|
150
|
+
"--out-name",
|
|
151
|
+
&out_name,
|
|
143
152
|
wasm_artifact.to_str().unwrap(),
|
|
144
153
|
])
|
|
145
154
|
.status()
|
|
146
155
|
.map_err(|e| WasmError {
|
|
147
|
-
message: format!(
|
|
156
|
+
message: format!(
|
|
157
|
+
"Failed to run wasm-bindgen: {}. Install with: cargo install wasm-bindgen-cli",
|
|
158
|
+
e
|
|
159
|
+
),
|
|
148
160
|
})?;
|
|
149
161
|
if !bindgen_status.success() {
|
|
150
|
-
return Err(WasmError {
|
|
162
|
+
return Err(WasmError {
|
|
163
|
+
message: "wasm-bindgen failed".to_string(),
|
|
164
|
+
});
|
|
151
165
|
}
|
|
152
166
|
let js_name = format!("{}.js", stem);
|
|
153
167
|
let html = format!(
|
|
@@ -171,7 +185,12 @@ run(chunk);
|
|
|
171
185
|
std::fs::write(&html_path, html).map_err(|e| WasmError {
|
|
172
186
|
message: format!("Cannot write {}: {}", html_path.display(), e),
|
|
173
187
|
})?;
|
|
174
|
-
println!(
|
|
188
|
+
println!(
|
|
189
|
+
"Built: {}_bg.wasm, {}.js, {}",
|
|
190
|
+
stem,
|
|
191
|
+
stem,
|
|
192
|
+
html_path.display()
|
|
193
|
+
);
|
|
175
194
|
Ok(())
|
|
176
195
|
}
|
|
177
196
|
|
|
@@ -233,9 +252,8 @@ pub fn compile_to_wasi(
|
|
|
233
252
|
message: format!("Cannot create output directory: {}", e),
|
|
234
253
|
})?;
|
|
235
254
|
|
|
236
|
-
let workspace_root =
|
|
237
|
-
message: e
|
|
238
|
-
})?;
|
|
255
|
+
let workspace_root =
|
|
256
|
+
tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
|
|
239
257
|
|
|
240
258
|
// Create generated project: wasi_build/{stem}/
|
|
241
259
|
let build_dir = out_dir_abs.join("wasi_build").join(stem);
|
|
@@ -249,9 +267,7 @@ pub fn compile_to_wasi(
|
|
|
249
267
|
})?;
|
|
250
268
|
|
|
251
269
|
// Cargo.toml - path to tishlang_wasm_runtime (crate in crates/tish_wasm_runtime)
|
|
252
|
-
let runtime_path = workspace_root
|
|
253
|
-
.join("crates")
|
|
254
|
-
.join("tish_wasm_runtime");
|
|
270
|
+
let runtime_path = workspace_root.join("crates").join("tish_wasm_runtime");
|
|
255
271
|
let runtime_path_str = runtime_path
|
|
256
272
|
.canonicalize()
|
|
257
273
|
.unwrap_or(runtime_path)
|
|
@@ -314,12 +330,7 @@ fn main() {
|
|
|
314
330
|
let build_status = Command::new(&cargo)
|
|
315
331
|
.current_dir(&build_dir)
|
|
316
332
|
.env("CARGO_TARGET_DIR", &target_dir)
|
|
317
|
-
.args([
|
|
318
|
-
"build",
|
|
319
|
-
"--target",
|
|
320
|
-
"wasm32-wasip1",
|
|
321
|
-
"--release",
|
|
322
|
-
])
|
|
333
|
+
.args(["build", "--target", "wasm32-wasip1", "--release"])
|
|
323
334
|
.status()
|
|
324
335
|
.map_err(|e| WasmError {
|
|
325
336
|
message: format!("Failed to run cargo: {}", e),
|
|
@@ -355,4 +366,3 @@ fn main() {
|
|
|
355
366
|
);
|
|
356
367
|
Ok(())
|
|
357
368
|
}
|
|
358
|
-
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_cargo_bindgen"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Generate Rust glue for Tish cargo: imports (bindgen-style, pre-commit friendly)"
|
|
6
|
+
license-file = { workspace = true }
|
|
7
|
+
repository = { workspace = true }
|
|
8
|
+
|
|
9
|
+
[[bin]]
|
|
10
|
+
name = "tishlang-cargo-bindgen"
|
|
11
|
+
path = "src/main.rs"
|
|
12
|
+
|
|
13
|
+
[lib]
|
|
14
|
+
name = "tishlang_cargo_bindgen"
|
|
15
|
+
path = "src/lib.rs"
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
cargo_metadata = "0.19"
|
|
19
|
+
clap = { version = "4.6", features = ["derive", "color"] }
|
|
20
|
+
pathdiff = "0.2"
|
|
21
|
+
serde_json = "1"
|
|
22
|
+
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
|
23
|
+
tempfile = "3"
|
|
24
|
+
toml = "0.8"
|
|
25
|
+
walkdir = "2"
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//! Classify a `pub fn` from syn for glue emission (driven by signature shape, not crate name).
|
|
2
|
+
|
|
3
|
+
use syn::{
|
|
4
|
+
FnArg, GenericArgument, ItemFn, PathArguments, ReturnType, Type, TypeReference,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
8
|
+
pub enum SignatureClass {
|
|
9
|
+
/// `fn foo(args: &[Value]) -> Value` (or `tishlang_runtime::Value`).
|
|
10
|
+
TishValueAbi,
|
|
11
|
+
/// First parameter `&T` (or `&mut T`), `T: Serialize` (or `?Sized + Serialize`), returns `Result<String, _>`-like.
|
|
12
|
+
SerializeRefToResultString,
|
|
13
|
+
/// First parameter `&str` (or `& 'a str`), returns `Result<_, _>`, and has `Deserialize` bound on a type param.
|
|
14
|
+
DeserializeStrToResult,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn classify_public_fn(item: &ItemFn) -> Option<SignatureClass> {
|
|
18
|
+
if matches!(classify_tish_abi(item), Some(SignatureClass::TishValueAbi)) {
|
|
19
|
+
return Some(SignatureClass::TishValueAbi);
|
|
20
|
+
}
|
|
21
|
+
if is_deserialize_str_result(item) {
|
|
22
|
+
return Some(SignatureClass::DeserializeStrToResult);
|
|
23
|
+
}
|
|
24
|
+
if is_serialize_ref_to_result_string(item) {
|
|
25
|
+
return Some(SignatureClass::SerializeRefToResultString);
|
|
26
|
+
}
|
|
27
|
+
None
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn classify_tish_abi(item: &ItemFn) -> Option<SignatureClass> {
|
|
31
|
+
let sig = &item.sig;
|
|
32
|
+
let mut value_args = 0;
|
|
33
|
+
for arg in &sig.inputs {
|
|
34
|
+
let FnArg::Typed(t) = arg else {
|
|
35
|
+
continue;
|
|
36
|
+
};
|
|
37
|
+
if is_slice_value(&t.ty) {
|
|
38
|
+
value_args += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if value_args != 1 || sig.inputs.len() != 1 {
|
|
42
|
+
return None;
|
|
43
|
+
}
|
|
44
|
+
let Some(ret_ty) = return_type_inner(&sig.output) else {
|
|
45
|
+
return None;
|
|
46
|
+
};
|
|
47
|
+
if !is_value_type(ret_ty) {
|
|
48
|
+
return None;
|
|
49
|
+
}
|
|
50
|
+
Some(SignatureClass::TishValueAbi)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn return_type_inner(ret: &ReturnType) -> Option<&Type> {
|
|
54
|
+
match ret {
|
|
55
|
+
ReturnType::Default => None,
|
|
56
|
+
ReturnType::Type(_, ty) => Some(ty),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn is_slice_value(ty: &Type) -> bool {
|
|
61
|
+
let Some(inner) = strip_reference(ty) else {
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
let Type::Slice(s) = inner else {
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
is_value_type(&s.elem)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn strip_reference(ty: &Type) -> Option<&Type> {
|
|
71
|
+
match ty {
|
|
72
|
+
Type::Reference(TypeReference { elem, .. }) => Some(elem.as_ref()),
|
|
73
|
+
_ => None,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn is_value_type(ty: &Type) -> bool {
|
|
78
|
+
let Type::Path(p) = ty else {
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
let seg = p.path.segments.last();
|
|
82
|
+
let Some(seg) = seg else {
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
if seg.ident != "Value" {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Accept `Value`, `tishlang_runtime::Value`, `tishlang_core::Value`
|
|
89
|
+
if p.path.segments.len() == 1 {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
let prev = &p.path.segments[p.path.segments.len() - 2];
|
|
93
|
+
prev.ident == "tishlang_runtime" || prev.ident == "tishlang_core"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn is_str_ref(ty: &Type) -> bool {
|
|
97
|
+
match ty {
|
|
98
|
+
Type::Reference(TypeReference { elem, .. }) => matches!(
|
|
99
|
+
elem.as_ref(),
|
|
100
|
+
Type::Path(p) if p.path.is_ident("str")
|
|
101
|
+
),
|
|
102
|
+
_ => false,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn is_deserialize_str_result(item: &ItemFn) -> bool {
|
|
107
|
+
let sig = &item.sig;
|
|
108
|
+
if sig.inputs.len() != 1 {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
let FnArg::Typed(arg) = sig.inputs.first().unwrap() else {
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
if !is_str_ref(&arg.ty) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
let Some(ret) = return_type_inner(&sig.output) else {
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
if result_ok_type(ret).is_none() {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
has_deserialize_bound(item)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn has_deserialize_bound(item: &ItemFn) -> bool {
|
|
127
|
+
for p in &item.sig.generics.params {
|
|
128
|
+
if let syn::GenericParam::Type(t) = p {
|
|
129
|
+
for b in &t.bounds {
|
|
130
|
+
if bound_name_is(b, "Deserialize") {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if let Some(wc) = &item.sig.generics.where_clause {
|
|
137
|
+
for pred in &wc.predicates {
|
|
138
|
+
if let syn::WherePredicate::Type(t) = pred {
|
|
139
|
+
for b in &t.bounds {
|
|
140
|
+
if bound_name_is(b, "Deserialize") {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn bound_name_is(b: &syn::TypeParamBound, want: &str) -> bool {
|
|
151
|
+
let syn::TypeParamBound::Trait(t) = b else {
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
154
|
+
let path = &t.path;
|
|
155
|
+
path.segments.last().is_some_and(|s| s.ident == want)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn is_serialize_ref_to_result_string(item: &ItemFn) -> bool {
|
|
159
|
+
let sig = &item.sig;
|
|
160
|
+
if sig.inputs.len() != 1 {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
let FnArg::Typed(arg) = sig.inputs.first().unwrap() else {
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
if strip_reference(&arg.ty).is_none() {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
let Some(ret) = return_type_inner(&sig.output) else {
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
let Some(ok_ty) = result_ok_type(ret) else {
|
|
173
|
+
return false;
|
|
174
|
+
};
|
|
175
|
+
if !type_is_string_or_str(ok_ty) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
has_serialize_bound(item)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn has_serialize_bound(item: &ItemFn) -> bool {
|
|
182
|
+
for p in &item.sig.generics.params {
|
|
183
|
+
if let syn::GenericParam::Type(t) = p {
|
|
184
|
+
for b in &t.bounds {
|
|
185
|
+
if bound_name_is(b, "Serialize") {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if let Some(wc) = &item.sig.generics.where_clause {
|
|
192
|
+
for pred in &wc.predicates {
|
|
193
|
+
if let syn::WherePredicate::Type(t) = pred {
|
|
194
|
+
for b in &t.bounds {
|
|
195
|
+
if bound_name_is(b, "Serialize") {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn result_ok_type(ty: &Type) -> Option<&Type> {
|
|
206
|
+
let Type::Path(p) = ty else {
|
|
207
|
+
return None;
|
|
208
|
+
};
|
|
209
|
+
let seg = p.path.segments.last()?;
|
|
210
|
+
if seg.ident != "Result" {
|
|
211
|
+
return None;
|
|
212
|
+
}
|
|
213
|
+
let PathArguments::AngleBracketed(ab) = &seg.arguments else {
|
|
214
|
+
return None;
|
|
215
|
+
};
|
|
216
|
+
let first = ab.args.first()?;
|
|
217
|
+
let GenericArgument::Type(t) = first else {
|
|
218
|
+
return None;
|
|
219
|
+
};
|
|
220
|
+
Some(t)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn type_is_string_or_str(ty: &Type) -> bool {
|
|
224
|
+
match ty {
|
|
225
|
+
Type::Path(p) => {
|
|
226
|
+
if p.path.is_ident("String") {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
p.path.segments.len() == 1 && p.path.segments[0].ident == "str"
|
|
230
|
+
}
|
|
231
|
+
_ => false,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[cfg(test)]
|
|
236
|
+
mod tests {
|
|
237
|
+
use super::*;
|
|
238
|
+
use syn::parse_quote;
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn classify_serde_to_string_shape() {
|
|
242
|
+
let item: ItemFn = parse_quote! {
|
|
243
|
+
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> Result<String, ()> {
|
|
244
|
+
unimplemented!()
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
assert_eq!(
|
|
248
|
+
classify_public_fn(&item),
|
|
249
|
+
Some(SignatureClass::SerializeRefToResultString)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn classify_from_str_shape() {
|
|
255
|
+
let item: ItemFn = parse_quote! {
|
|
256
|
+
pub fn from_str<'a, T: Deserialize<'a>>(s: &'a str) -> Result<T, ()> {
|
|
257
|
+
unimplemented!()
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
assert_eq!(
|
|
261
|
+
classify_public_fn(&item),
|
|
262
|
+
Some(SignatureClass::DeserializeStrToResult)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//! Walk dependency `src/**/*.rs` and collect `pub fn` items by name.
|
|
2
|
+
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
|
|
7
|
+
use syn::{Item, ItemFn, Visibility};
|
|
8
|
+
use walkdir::WalkDir;
|
|
9
|
+
|
|
10
|
+
fn is_pub(vis: &Visibility) -> bool {
|
|
11
|
+
matches!(vis, Visibility::Public(_))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Map export name (Rust ident) to the function AST (must be unique).
|
|
15
|
+
pub fn discover_public_functions(crate_root: &Path) -> Result<HashMap<String, ItemFn>, String> {
|
|
16
|
+
let src = crate_root.join("src");
|
|
17
|
+
if !src.is_dir() {
|
|
18
|
+
return Err(format!("no src/ under {}", crate_root.display()));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let mut map: HashMap<String, (PathBuf, ItemFn)> = HashMap::new();
|
|
22
|
+
|
|
23
|
+
for entry in WalkDir::new(&src)
|
|
24
|
+
.into_iter()
|
|
25
|
+
.filter_map(|e| e.ok())
|
|
26
|
+
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
|
|
27
|
+
{
|
|
28
|
+
let path = entry.path();
|
|
29
|
+
let text = fs::read_to_string(path).map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
30
|
+
let file = syn::parse_file(&text).map_err(|e| format!("parse {}: {}", path.display(), e))?;
|
|
31
|
+
|
|
32
|
+
for item in file.items {
|
|
33
|
+
if let Item::Fn(f) = item {
|
|
34
|
+
if !is_pub(&f.vis) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let name = f.sig.ident.to_string();
|
|
38
|
+
if let Some((prev_path, _)) = map.get(&name) {
|
|
39
|
+
return Err(format!(
|
|
40
|
+
"ambiguous public fn `{}`: found in {} and {}",
|
|
41
|
+
name,
|
|
42
|
+
prev_path.display(),
|
|
43
|
+
path.display()
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
map.insert(name, (path.to_path_buf(), f));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Ok(map.into_iter().map(|(k, (_, v))| (k, v)).collect())
|
|
52
|
+
}
|