@tishlang/tish 2.2.0 → 2.2.4
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/bin/tish +0 -0
- package/crates/tish/Cargo.toml +7 -3
- package/crates/tish/src/main.rs +26 -8
- package/crates/tish_ast/src/ast.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +0 -4
- package/crates/tish_build_utils/src/lib.rs +0 -41
- package/crates/tish_bytecode/Cargo.toml +0 -1
- package/crates/tish_compile/src/codegen.rs +21 -0
- package/crates/tish_cranelift/Cargo.toml +0 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +0 -1
- package/crates/tish_eval/Cargo.toml +1 -6
- package/crates/tish_fmt/src/lib.rs +156 -75
- package/crates/tish_lsp/src/import_goto.rs +25 -3
- package/crates/tish_lsp/src/main.rs +250 -5
- package/crates/tish_parser/src/parser.rs +20 -8
- package/crates/tish_resolve/src/lib.rs +775 -55
- package/crates/tish_runtime/Cargo.toml +0 -4
- package/crates/tishlang_cargo_bindgen/Cargo.toml +7 -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
package/bin/tish
CHANGED
|
Binary file
|
package/crates/tish/Cargo.toml
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tishlang"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.4"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Tish CLI - run, REPL, compile to native"
|
|
6
6
|
license-file = { workspace = true }
|
|
7
7
|
repository = { workspace = true }
|
|
8
8
|
|
|
9
|
+
# tishlang_runtime has no direct source use here — it's required by the http/fs/process/
|
|
10
|
+
# regex/ws/tty feature-forwards below (`tishlang_runtime/<feat>`). cargo-machete only scans
|
|
11
|
+
# source, so it false-flags it; ignore it.
|
|
12
|
+
[package.metadata.cargo-machete]
|
|
13
|
+
ignored = ["tishlang_runtime"]
|
|
14
|
+
|
|
9
15
|
[[bin]]
|
|
10
16
|
name = "tish"
|
|
11
17
|
path = "src/main.rs"
|
|
@@ -38,7 +44,6 @@ tty = ["tishlang_eval/tty", "tishlang_runtime/tty", "tishlang_compil
|
|
|
38
44
|
# GPU compute — Apple Silicon only
|
|
39
45
|
[dependencies]
|
|
40
46
|
rustyline = { version = "17", features = ["with-file-history"] }
|
|
41
|
-
tishlang_lexer = { path = "../tish_lexer", version = ">=0.1" }
|
|
42
47
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
43
48
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
44
49
|
tishlang_eval = { path = "../tish_eval", version = ">=0.1" }
|
|
@@ -49,7 +54,6 @@ tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
|
|
|
49
54
|
tishlang_vm = { path = "../tish_vm", version = ">=0.1" }
|
|
50
55
|
tishlang_ffi = { path = "../tish_ffi", version = ">=0.1" }
|
|
51
56
|
tishlang_native = { path = "../tish_native", version = ">=0.1" }
|
|
52
|
-
tishlang_llvm = { path = "../tish_llvm", version = ">=0.1" }
|
|
53
57
|
tishlang_wasm = { path = "../tish_wasm", version = ">=0.1" }
|
|
54
58
|
tishlang_runtime = { path = "../tish_runtime", version = ">=0.1" }
|
|
55
59
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
package/crates/tish/src/main.rs
CHANGED
|
@@ -56,15 +56,17 @@ fn vm_capabilities_for_cli_run(cli_features: &[String]) -> HashSet<String> {
|
|
|
56
56
|
|
|
57
57
|
/// `--feature` list for `tish build --target native`: same default as `tish run` (all linked-in caps).
|
|
58
58
|
fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
|
|
59
|
-
if cli_features.is_empty() {
|
|
60
|
-
|
|
61
|
-
.into_iter()
|
|
62
|
-
.collect();
|
|
63
|
-
v.sort();
|
|
64
|
-
v
|
|
59
|
+
let mut v: Vec<String> = if cli_features.is_empty() {
|
|
60
|
+
tishlang_vm::all_compiled_capabilities().into_iter().collect()
|
|
65
61
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
// Normalize exactly like `tish run`: split comma-separated values (`--feature http,timers`)
|
|
63
|
+
// and expand `full`. Without this, a value like "http,timers,process" was treated as one
|
|
64
|
+
// opaque feature name, so `has_feature("http")` was false and the http runtime imports were
|
|
65
|
+
// never emitted — async `fetch` then failed to compile (issue #209).
|
|
66
|
+
normalize_capability_flags(cli_features).into_iter().collect()
|
|
67
|
+
};
|
|
68
|
+
v.sort();
|
|
69
|
+
v
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/// `tish script.tish` → insert `run` so it matches `tish run script.tish` (npx / npm UX).
|
|
@@ -829,6 +831,22 @@ mod cli_tests {
|
|
|
829
831
|
}
|
|
830
832
|
}
|
|
831
833
|
|
|
834
|
+
#[test]
|
|
835
|
+
fn native_build_features_split_commas_and_expand_full() {
|
|
836
|
+
use super::native_build_features_from_cli;
|
|
837
|
+
// Issue #209: a comma-joined value was treated as one opaque feature, so has_feature("http")
|
|
838
|
+
// was false and the native http imports were skipped (async `fetch` failed to compile).
|
|
839
|
+
let f = native_build_features_from_cli(&["http,timers,process".to_string()]);
|
|
840
|
+
for cap in ["http", "timers", "process"] {
|
|
841
|
+
assert!(f.iter().any(|s| s == cap), "missing `{cap}` in {f:?}");
|
|
842
|
+
}
|
|
843
|
+
// `full` expands to every capability, matching `tish run`.
|
|
844
|
+
let full = native_build_features_from_cli(&["full".to_string()]);
|
|
845
|
+
for cap in ["http", "timers", "fs", "process", "regex", "ws", "tty"] {
|
|
846
|
+
assert!(full.iter().any(|s| s == cap), "`full` missing `{cap}` in {full:?}");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
832
850
|
#[test]
|
|
833
851
|
fn run_stdin_marker_parses_as_file() {
|
|
834
852
|
let cli = Cli::try_parse_from(["tish", "run", "-"]).unwrap();
|
|
@@ -436,6 +436,11 @@ pub enum Expr {
|
|
|
436
436
|
/// JSX element: <Tag props>children</Tag>
|
|
437
437
|
JsxElement {
|
|
438
438
|
tag: Arc<str>,
|
|
439
|
+
/// Span of the tag name in the opening tag (`<`**`Foo`**`>`), for go-to-def / rename.
|
|
440
|
+
tag_span: Span,
|
|
441
|
+
/// Span of the tag name in the closing tag (`</`**`Foo`**`>`); `None` for self-closing
|
|
442
|
+
/// (`<Foo/>`). Renaming a component must update both, or `<Bar></Foo>` would be invalid.
|
|
443
|
+
close_tag_span: Option<Span>,
|
|
439
444
|
props: Vec<JsxProp>,
|
|
440
445
|
children: Vec<JsxChild>,
|
|
441
446
|
span: Span,
|
|
@@ -5,7 +5,3 @@ edition = "2021"
|
|
|
5
5
|
description = "Shared build utilities for Tish (workspace discovery, path resolution)"
|
|
6
6
|
license-file = { workspace = true }
|
|
7
7
|
repository = { workspace = true }
|
|
8
|
-
|
|
9
|
-
[dependencies]
|
|
10
|
-
# Bundled `protoc` for nested `cargo build` (Lance / prost-build) when none is on PATH.
|
|
11
|
-
protoc-bin-vendored = "3"
|
|
@@ -355,35 +355,6 @@ pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String>
|
|
|
355
355
|
Ok(build_dir)
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
/// `PROTOC` for prost-build in transitive crates (e.g. lance-encoding) during nested `cargo build`.
|
|
359
|
-
/// Respects an existing `PROTOC`, then `protoc` on `PATH`, then `protoc-bin-vendored`.
|
|
360
|
-
fn protoc_for_nested_cargo() -> Option<PathBuf> {
|
|
361
|
-
// Empty or non-file PROTOC must not short-circuit: prost-build treats "" like a missing binary.
|
|
362
|
-
if let Some(v) = std::env::var_os("PROTOC") {
|
|
363
|
-
if !v.is_empty() {
|
|
364
|
-
let p = PathBuf::from(&v);
|
|
365
|
-
if p.is_file() {
|
|
366
|
-
return None;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
// Prefer vendored protoc over PATH: a broken or non-executable `protoc` on PATH still passes `is_file()`.
|
|
371
|
-
if let Ok(p) = protoc_bin_vendored::protoc_bin_path() {
|
|
372
|
-
return Some(p);
|
|
373
|
-
}
|
|
374
|
-
let ext = if cfg!(windows) { ".exe" } else { "" };
|
|
375
|
-
let name = format!("protoc{}", ext);
|
|
376
|
-
if let Some(paths) = std::env::var_os("PATH") {
|
|
377
|
-
for dir in std::env::split_paths(&paths) {
|
|
378
|
-
let candidate = dir.join(&name);
|
|
379
|
-
if candidate.is_file() {
|
|
380
|
-
return Some(candidate);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
None
|
|
385
|
-
}
|
|
386
|
-
|
|
387
358
|
/// Run cargo build in the given directory.
|
|
388
359
|
/// If `cross_target` is Some, passes `--target` and skips `-C target-cpu=native`.
|
|
389
360
|
pub fn run_cargo_build(
|
|
@@ -433,9 +404,6 @@ pub fn run_cargo_build(
|
|
|
433
404
|
} else {
|
|
434
405
|
cmd.env_remove("CARGO_INCREMENTAL");
|
|
435
406
|
}
|
|
436
|
-
if let Some(protoc) = protoc_for_nested_cargo() {
|
|
437
|
-
cmd.env("PROTOC", protoc);
|
|
438
|
-
}
|
|
439
407
|
let output = cmd
|
|
440
408
|
.output()
|
|
441
409
|
.map_err(|e| format!("Failed to run cargo: {}", e))?;
|
|
@@ -460,15 +428,6 @@ mod protoc_tests {
|
|
|
460
428
|
assert_eq!(cargo_target_name("hello-ios"), "hello_ios");
|
|
461
429
|
assert_eq!(cargo_target_name("tish_out"), "tish_out");
|
|
462
430
|
}
|
|
463
|
-
|
|
464
|
-
#[test]
|
|
465
|
-
fn protoc_for_nested_cargo_without_env_uses_vendored_or_path() {
|
|
466
|
-
let _lock = std::sync::Mutex::new(());
|
|
467
|
-
let _guard = _lock.lock().unwrap();
|
|
468
|
-
std::env::remove_var("PROTOC");
|
|
469
|
-
let p = protoc_for_nested_cargo().expect("expected vendored or PATH protoc");
|
|
470
|
-
assert!(p.exists(), "resolved protoc should exist: {}", p.display());
|
|
471
|
-
}
|
|
472
431
|
}
|
|
473
432
|
|
|
474
433
|
/// Find the built static library in target/release (or target/$TRIPLE/release).
|
|
@@ -11,7 +11,6 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
|
11
11
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
12
12
|
|
|
13
13
|
[dev-dependencies]
|
|
14
|
-
tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
|
|
15
14
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
16
15
|
tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
|
|
17
16
|
tishlang_vm = { path = "../tish_vm", version = ">=0.1" }
|
|
@@ -2674,6 +2674,8 @@ impl Codegen {
|
|
|
2674
2674
|
"Set",
|
|
2675
2675
|
"Map",
|
|
2676
2676
|
"Object",
|
|
2677
|
+
"Array",
|
|
2678
|
+
"Number",
|
|
2677
2679
|
"Float64Array",
|
|
2678
2680
|
"Float32Array",
|
|
2679
2681
|
"Int8Array",
|
|
@@ -2714,6 +2716,25 @@ impl Codegen {
|
|
|
2714
2716
|
self.writeln(&format!("let {} = {}.clone();", builtin, builtin));
|
|
2715
2717
|
}
|
|
2716
2718
|
}
|
|
2719
|
+
// Feature-gated globals also move into the closure when referenced.
|
|
2720
|
+
// Clone them only when their capability is actually linked, so we
|
|
2721
|
+
// never emit `let h = h.clone();` for a binding that was never
|
|
2722
|
+
// emitted (e.g. a fn-local named `h` in a program without JSX).
|
|
2723
|
+
let mut gated: Vec<&str> = Vec::new();
|
|
2724
|
+
if self.has_feature("http") {
|
|
2725
|
+
gated.extend(["fetch", "fetchAll"]);
|
|
2726
|
+
}
|
|
2727
|
+
if self.has_feature("fs") {
|
|
2728
|
+
gated.extend(["readFile", "writeFile", "fileExists", "isDir", "readDir", "mkdir"]);
|
|
2729
|
+
}
|
|
2730
|
+
if self.program_has_jsx && !self.has_native_ui_host {
|
|
2731
|
+
gated.extend(["Fragment", "h", "text", "useState", "createRoot"]);
|
|
2732
|
+
}
|
|
2733
|
+
for name in gated {
|
|
2734
|
+
if referenced.contains(name) {
|
|
2735
|
+
self.writeln(&format!("let {} = {}.clone();", name, name));
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2717
2738
|
self.writeln("Value::native(move |args: &[Value]| {");
|
|
2718
2739
|
self.value_fn_depth += 1;
|
|
2719
2740
|
self.indent += 1;
|
|
@@ -9,11 +9,7 @@ repository = { workspace = true }
|
|
|
9
9
|
[dependencies]
|
|
10
10
|
tishlang_build_utils = { path = "../tish_build_utils", version = ">=0.1" }
|
|
11
11
|
cranelift = "0.130"
|
|
12
|
-
cranelift-codegen = "0.130"
|
|
13
|
-
cranelift-frontend = "0.130"
|
|
14
12
|
cranelift-module = "0.130"
|
|
15
13
|
cranelift-native = "0.130"
|
|
16
14
|
cranelift-object = "0.130"
|
|
17
|
-
target-lexicon = "0.13"
|
|
18
15
|
tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
|
|
19
|
-
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
@@ -13,8 +13,6 @@ timers = []
|
|
|
13
13
|
http = [
|
|
14
14
|
"timers",
|
|
15
15
|
"tokio",
|
|
16
|
-
"reqwest",
|
|
17
|
-
"futures",
|
|
18
16
|
"tiny_http",
|
|
19
17
|
"tishlang_core/regex",
|
|
20
18
|
"dep:tishlang_runtime",
|
|
@@ -27,7 +25,7 @@ http = [
|
|
|
27
25
|
fs = []
|
|
28
26
|
process = []
|
|
29
27
|
tty = ["dep:tishlang_runtime", "tishlang_runtime/tty"]
|
|
30
|
-
regex = ["
|
|
28
|
+
regex = ["tishlang_core/regex"]
|
|
31
29
|
tokio = ["dep:tokio"]
|
|
32
30
|
ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
|
|
33
31
|
|
|
@@ -43,9 +41,6 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
|
43
41
|
tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
44
42
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
45
43
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
46
|
-
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"], optional = true }
|
|
47
|
-
futures = { version = "0.3.32", optional = true }
|
|
48
44
|
tiny_http = { version = "0.12.0", optional = true }
|
|
49
|
-
fancy-regex = { version = "0.17.0", optional = true }
|
|
50
45
|
tokio = { version = "1.50.0", features = ["rt-multi-thread", "time", "sync"], optional = true }
|
|
51
46
|
tishlang_runtime = { path = "../tish_runtime", version = ">=0.1", optional = true }
|
|
@@ -1124,11 +1124,41 @@ impl Printer {
|
|
|
1124
1124
|
}
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
/// Print JSX children inline and verbatim. JSX whitespace is significant in tish, so text is
|
|
1128
|
+
/// emitted exactly as written and there is no reflow/indentation. A nested element/fragment is
|
|
1129
|
+
/// printed bare (as a child element); any other expression child gets `{ }`.
|
|
1130
|
+
fn jsx_children(&mut self, children: &[JsxChild]) {
|
|
1131
|
+
for ch in children {
|
|
1132
|
+
match ch {
|
|
1133
|
+
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
1134
|
+
JsxChild::Expr(e) => {
|
|
1135
|
+
if matches!(e, Expr::JsxElement { .. } | Expr::JsxFragment { .. }) {
|
|
1136
|
+
self.expr(e);
|
|
1137
|
+
} else {
|
|
1138
|
+
self.buf.push('{');
|
|
1139
|
+
self.expr(e);
|
|
1140
|
+
self.buf.push('}');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1127
1147
|
fn expr(&mut self, e: &Expr) {
|
|
1128
1148
|
match e {
|
|
1129
1149
|
Expr::Literal { value, .. } => match value {
|
|
1130
1150
|
Literal::Number(n) => {
|
|
1131
|
-
if n.
|
|
1151
|
+
if !n.is_finite() {
|
|
1152
|
+
// f64 overflow / NaN: emit the tish globals, not Rust's bare `inf`/`-inf`/`NaN`
|
|
1153
|
+
// (`inf` would re-parse as an undefined identifier).
|
|
1154
|
+
self.buf.push_str(if n.is_nan() {
|
|
1155
|
+
"NaN"
|
|
1156
|
+
} else if *n > 0.0 {
|
|
1157
|
+
"Infinity"
|
|
1158
|
+
} else {
|
|
1159
|
+
"-Infinity"
|
|
1160
|
+
});
|
|
1161
|
+
} else if n.fract() == 0.0 && n.abs() < 1e15 {
|
|
1132
1162
|
self.buf.push_str(&format!("{}", *n as i64));
|
|
1133
1163
|
} else {
|
|
1134
1164
|
self.buf.push_str(&format!("{}", n));
|
|
@@ -1169,7 +1199,18 @@ impl Printer {
|
|
|
1169
1199
|
}
|
|
1170
1200
|
Expr::New { callee, args, .. } => {
|
|
1171
1201
|
self.buf.push_str("new ");
|
|
1172
|
-
|
|
1202
|
+
// `new` parses its callee as a member expression WITHOUT a trailing call, so any call
|
|
1203
|
+
// in the callee's spine (`new (factory())()`, `new (factory().Cls)()`) must be
|
|
1204
|
+
// parenthesized — otherwise the printed `()` rebinds as the constructor's argument
|
|
1205
|
+
// list. child()'s precedence check can't catch this because a Call/Member already has
|
|
1206
|
+
// postfix precedence.
|
|
1207
|
+
if new_callee_has_call(callee) {
|
|
1208
|
+
self.buf.push('(');
|
|
1209
|
+
self.expr(callee);
|
|
1210
|
+
self.buf.push(')');
|
|
1211
|
+
} else {
|
|
1212
|
+
self.child(callee, PREC_POSTFIX);
|
|
1213
|
+
}
|
|
1173
1214
|
if !args.is_empty() {
|
|
1174
1215
|
self.emit_args(args);
|
|
1175
1216
|
}
|
|
@@ -1370,72 +1411,21 @@ impl Printer {
|
|
|
1370
1411
|
if children.is_empty() {
|
|
1371
1412
|
self.buf.push_str(" />");
|
|
1372
1413
|
} else {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
self.buf.push_str("</");
|
|
1384
|
-
self.buf.push_str(tag.as_ref());
|
|
1385
|
-
self.buf.push('>');
|
|
1386
|
-
} else {
|
|
1387
|
-
self.buf.push('>');
|
|
1388
|
-
self.buf.push('\n');
|
|
1389
|
-
for ch in children {
|
|
1390
|
-
self.buf.push_str(" ");
|
|
1391
|
-
match ch {
|
|
1392
|
-
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
1393
|
-
JsxChild::Expr(e) => {
|
|
1394
|
-
self.buf.push('{');
|
|
1395
|
-
self.expr(e);
|
|
1396
|
-
self.buf.push('}');
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
self.buf.push('\n');
|
|
1400
|
-
}
|
|
1401
|
-
self.buf.push_str(" </");
|
|
1402
|
-
self.buf.push_str(tag.as_ref());
|
|
1403
|
-
self.buf.push('>');
|
|
1404
|
-
}
|
|
1414
|
+
// JSX whitespace is significant in tish (the lexer keeps it verbatim and codegen
|
|
1415
|
+
// emits it as content), so children are printed inline and verbatim. Reflowing
|
|
1416
|
+
// them onto indented lines injected newlines/spaces as real text nodes, which
|
|
1417
|
+
// changed the rendered output and was non-idempotent.
|
|
1418
|
+
self.buf.push('>');
|
|
1419
|
+
self.jsx_children(children);
|
|
1420
|
+
self.buf.push_str("</");
|
|
1421
|
+
self.buf.push_str(tag.as_ref());
|
|
1422
|
+
self.buf.push('>');
|
|
1405
1423
|
}
|
|
1406
1424
|
}
|
|
1407
1425
|
Expr::JsxFragment { children, .. } => {
|
|
1408
1426
|
self.buf.push_str("<>");
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
} else {
|
|
1412
|
-
let compact = children.len() == 1
|
|
1413
|
-
&& matches!(
|
|
1414
|
-
&children[0],
|
|
1415
|
-
JsxChild::Text(t) if !t.as_ref().contains('\n')
|
|
1416
|
-
);
|
|
1417
|
-
if compact {
|
|
1418
|
-
if let JsxChild::Text(t) = &children[0] {
|
|
1419
|
-
self.buf.push_str(t.as_ref());
|
|
1420
|
-
}
|
|
1421
|
-
self.buf.push_str("</>");
|
|
1422
|
-
} else {
|
|
1423
|
-
self.buf.push('\n');
|
|
1424
|
-
for ch in children {
|
|
1425
|
-
self.buf.push_str(" ");
|
|
1426
|
-
match ch {
|
|
1427
|
-
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
1428
|
-
JsxChild::Expr(e) => {
|
|
1429
|
-
self.buf.push('{');
|
|
1430
|
-
self.expr(e);
|
|
1431
|
-
self.buf.push('}');
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
self.buf.push('\n');
|
|
1435
|
-
}
|
|
1436
|
-
self.buf.push_str("</>");
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1427
|
+
self.jsx_children(children);
|
|
1428
|
+
self.buf.push_str("</>");
|
|
1439
1429
|
}
|
|
1440
1430
|
Expr::NativeModuleLoad {
|
|
1441
1431
|
spec, export_name, ..
|
|
@@ -1466,9 +1456,19 @@ impl Printer {
|
|
|
1466
1456
|
}
|
|
1467
1457
|
|
|
1468
1458
|
fn escape_template(s: &str) -> String {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1459
|
+
// In a template literal only backslash, backtick, and a `$` that begins an interpolation (`${`)
|
|
1460
|
+
// need escaping. A bare `$` is literal, so escaping every `$` just adds spurious backslashes.
|
|
1461
|
+
let mut out = String::with_capacity(s.len());
|
|
1462
|
+
let mut chars = s.chars().peekable();
|
|
1463
|
+
while let Some(c) = chars.next() {
|
|
1464
|
+
match c {
|
|
1465
|
+
'\\' => out.push_str("\\\\"),
|
|
1466
|
+
'`' => out.push_str("\\`"),
|
|
1467
|
+
'$' if chars.peek() == Some(&'{') => out.push_str("\\$"),
|
|
1468
|
+
_ => out.push(c),
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
out
|
|
1472
1472
|
}
|
|
1473
1473
|
|
|
1474
1474
|
// Expression precedence levels (higher binds tighter), mirroring the parser's
|
|
@@ -1528,16 +1528,29 @@ fn expr_prec(e: &Expr) -> u8 {
|
|
|
1528
1528
|
| Expr::PrefixInc { .. }
|
|
1529
1529
|
| Expr::PrefixDec { .. } => 14,
|
|
1530
1530
|
Expr::Call { .. }
|
|
1531
|
-
| Expr::New { .. }
|
|
1532
1531
|
| Expr::Member { .. }
|
|
1533
1532
|
| Expr::Index { .. }
|
|
1534
1533
|
| Expr::PostfixInc { .. }
|
|
1535
1534
|
| Expr::PostfixDec { .. } => PREC_POSTFIX,
|
|
1535
|
+
// `new X` binds looser than member/call, so a New used as the object of `.`/`[]`/`()`
|
|
1536
|
+
// must be parenthesized (e.g. `(new Foo()).bar()`, not `new Foo().bar()`).
|
|
1537
|
+
Expr::New { .. } => PREC_POSTFIX - 1,
|
|
1536
1538
|
Expr::ArrowFunction { .. } => PREC_ATOM,
|
|
1537
1539
|
_ => PREC_ATOM,
|
|
1538
1540
|
}
|
|
1539
1541
|
}
|
|
1540
1542
|
|
|
1543
|
+
/// True when a `new` callee contains a call in its member/index spine. Such a callee must be
|
|
1544
|
+
/// parenthesized, because `new` parses its callee without a trailing call and would otherwise bind
|
|
1545
|
+
/// the first `()` as the constructor's argument list (`new (f())()`, `new (f().C)()`).
|
|
1546
|
+
fn new_callee_has_call(e: &Expr) -> bool {
|
|
1547
|
+
match e {
|
|
1548
|
+
Expr::Call { .. } => true,
|
|
1549
|
+
Expr::Member { object, .. } | Expr::Index { object, .. } => new_callee_has_call(object),
|
|
1550
|
+
_ => false,
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1541
1554
|
fn binop(op: BinOp) -> &'static str {
|
|
1542
1555
|
match op {
|
|
1543
1556
|
BinOp::Add => "+",
|
|
@@ -1813,15 +1826,83 @@ mod tests {
|
|
|
1813
1826
|
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1814
1827
|
}
|
|
1815
1828
|
|
|
1829
|
+
/// Debug-render the parsed AST with all `Span { … }` contents removed, so two programs compare
|
|
1830
|
+
/// equal iff they are structurally identical (reformatting legitimately changes spans).
|
|
1831
|
+
fn structure(src: &str) -> String {
|
|
1832
|
+
let dbg = format!("{:#?}", tishlang_parser::parse(src).unwrap().statements);
|
|
1833
|
+
let mut out = String::new();
|
|
1834
|
+
let mut rest = dbg.as_str();
|
|
1835
|
+
while let Some(i) = rest.find("Span {") {
|
|
1836
|
+
out.push_str(&rest[..i]);
|
|
1837
|
+
match rest[i..].find('}') {
|
|
1838
|
+
Some(j) => rest = &rest[i + j + 1..],
|
|
1839
|
+
None => {
|
|
1840
|
+
rest = "";
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
out.push_str(rest);
|
|
1846
|
+
out
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
#[test]
|
|
1850
|
+
fn new_expr_preserves_structure_and_is_idempotent() {
|
|
1851
|
+
// Each input must format to a program with the SAME structure (modulo spans), and a second
|
|
1852
|
+
// format pass must be a no-op. These previously corrupted into structurally different
|
|
1853
|
+
// programs (Call<->New<->Member swaps) on format.
|
|
1854
|
+
for src in [
|
|
1855
|
+
"const d = (new Foo()).bar()\n",
|
|
1856
|
+
"const b = new (factory().Cls)()\n",
|
|
1857
|
+
"const x = new (getClass())(1)\n",
|
|
1858
|
+
"const e = new Foo()\n",
|
|
1859
|
+
"const f = new a.b.c()\n",
|
|
1860
|
+
"const g = new Foo(1, 2).baz\n",
|
|
1861
|
+
] {
|
|
1862
|
+
let out = format_source(src).unwrap();
|
|
1863
|
+
let out2 = format_source(&out).unwrap();
|
|
1864
|
+
assert_eq!(structure(src), structure(&out), "structure changed: {src:?} -> {out:?}");
|
|
1865
|
+
assert_eq!(out, out2, "not idempotent: {src:?} -> {out:?} -> {out2:?}");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
#[test]
|
|
1870
|
+
fn jsx_preserves_structure_and_is_idempotent() {
|
|
1871
|
+
// JSX whitespace is significant; formatting must not inject newlines/indentation as text
|
|
1872
|
+
// (which changed rendered output and was non-idempotent). Structure must be preserved.
|
|
1873
|
+
for src in [
|
|
1874
|
+
"let x = <div>a{b}</div>\n",
|
|
1875
|
+
"const e = <div>Hello {name}!</div>\n",
|
|
1876
|
+
"const e2 = <div><span>a</span><span>b</span></div>\n",
|
|
1877
|
+
"let y = <div> spaced text </div>\n",
|
|
1878
|
+
"let z = <>{a}{b}</>\n",
|
|
1879
|
+
"let s = <div a={1} b=\"x\">{c}</div>\n",
|
|
1880
|
+
] {
|
|
1881
|
+
let out = format_source(src).unwrap();
|
|
1882
|
+
let out2 = format_source(&out).unwrap();
|
|
1883
|
+
assert_eq!(structure(src), structure(&out), "JSX structure changed: {src:?} -> {out:?}");
|
|
1884
|
+
assert_eq!(out, out2, "JSX not idempotent: {src:?} -> {out:?} -> {out2:?}");
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1816
1888
|
#[test]
|
|
1817
|
-
fn
|
|
1818
|
-
|
|
1889
|
+
fn non_finite_number_literal_emits_valid_token() {
|
|
1890
|
+
// 1e400 overflows f64 to infinity; it must format to the `Infinity` global, not Rust's bare
|
|
1891
|
+
// `inf` (which would re-parse as an undefined identifier).
|
|
1892
|
+
let src = "let x = 1e400\n";
|
|
1819
1893
|
let out = format_source(src).unwrap();
|
|
1820
|
-
assert!(
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1894
|
+
assert!(out.contains("Infinity"), "expected Infinity, got {out:?}");
|
|
1895
|
+
tishlang_parser::parse(&out).expect("formatted output must parse");
|
|
1896
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
#[test]
|
|
1900
|
+
fn template_literal_escapes_only_interpolation_dollar() {
|
|
1901
|
+
let src = "let p = `cost: $5 and ${x} done`\n";
|
|
1902
|
+
let out = format_source(src).unwrap();
|
|
1903
|
+
assert!(out.contains("cost: $5"), "a bare $ must not be escaped: {out:?}");
|
|
1904
|
+
assert_eq!(structure(src), structure(&out), "structure changed: {src:?} -> {out:?}");
|
|
1905
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent: {out:?}");
|
|
1825
1906
|
}
|
|
1826
1907
|
|
|
1827
1908
|
#[test]
|
|
@@ -450,6 +450,21 @@ fn resolve_relative_tish(from_dir: &Path, from_s: &str, imported: &str) -> Optio
|
|
|
450
450
|
crate::find_export(&prog, imported, &u, &src)
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
/// Resolve a relative default import (`import X from "./m"`) to the `export default` in the source.
|
|
454
|
+
fn resolve_relative_tish_default(from_dir: &Path, from_s: &str) -> Option<Location> {
|
|
455
|
+
let target = from_dir.join(from_s.trim_start_matches("./"));
|
|
456
|
+
let target = if target.extension().is_none() {
|
|
457
|
+
target.with_extension("tish")
|
|
458
|
+
} else {
|
|
459
|
+
target
|
|
460
|
+
};
|
|
461
|
+
let can = target.canonicalize().ok()?;
|
|
462
|
+
let u = Url::from_file_path(&can).ok()?;
|
|
463
|
+
let src = std::fs::read_to_string(&can).ok()?;
|
|
464
|
+
let prog = tishlang_parser::parse(&src).ok()?;
|
|
465
|
+
crate::find_default_export(&prog, &u, &src)
|
|
466
|
+
}
|
|
467
|
+
|
|
453
468
|
/// After same-file [`tishlang_resolve::definition_span`] misses, resolve import sites (Tish files,
|
|
454
469
|
/// `node_modules` packages, and Rust `pub fn` for native / `cargo:` imports).
|
|
455
470
|
pub fn definition_for_import(
|
|
@@ -474,12 +489,13 @@ pub fn definition_for_import(
|
|
|
474
489
|
};
|
|
475
490
|
let from_s = from.as_ref();
|
|
476
491
|
for sp in specifiers {
|
|
477
|
-
let (imported, local) = match sp {
|
|
492
|
+
let (imported, local, is_default) = match sp {
|
|
478
493
|
ImportSpecifier::Named { name, alias, .. } => (
|
|
479
494
|
name.as_ref(),
|
|
480
495
|
alias.as_ref().map(|a| a.as_ref()).unwrap_or(name.as_ref()),
|
|
496
|
+
false,
|
|
481
497
|
),
|
|
482
|
-
ImportSpecifier::Default { name, .. } => (name.as_ref(), name.as_ref()),
|
|
498
|
+
ImportSpecifier::Default { name, .. } => (name.as_ref(), name.as_ref(), true),
|
|
483
499
|
ImportSpecifier::Namespace { .. } => continue,
|
|
484
500
|
};
|
|
485
501
|
if local != word {
|
|
@@ -487,7 +503,13 @@ pub fn definition_for_import(
|
|
|
487
503
|
}
|
|
488
504
|
|
|
489
505
|
if from_s.starts_with("./") || from_s.starts_with("../") {
|
|
490
|
-
|
|
506
|
+
// A default import binds the source module's `export default`, which has no name to
|
|
507
|
+
// match; resolve it directly. Named imports resolve by exported name.
|
|
508
|
+
return if is_default {
|
|
509
|
+
resolve_relative_tish_default(from_dir, from_s)
|
|
510
|
+
} else {
|
|
511
|
+
resolve_relative_tish(from_dir, from_s, imported)
|
|
512
|
+
};
|
|
491
513
|
}
|
|
492
514
|
|
|
493
515
|
if is_native_import(from_s) {
|