@tishlang/tish-lint 3.10.3 → 3.10.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-lint +0 -0
- package/crates/tish_core/src/value.rs +80 -1
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_runtime/src/fs_ext.rs +28 -19
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
package/bin/tish-lint
CHANGED
|
Binary file
|
|
@@ -865,6 +865,51 @@ impl PropMap {
|
|
|
865
865
|
None
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
+
/// `insert` minus the hidden-class bookkeeping: never calls `shape::transition` and leaves
|
|
869
|
+
/// `self.shape` untouched. Only for construction paths that END at [`crate::shape::DICT_SHAPE`]
|
|
870
|
+
/// (see [`PropMap::from_unordered`]) — a map built through this and left with a real shape id
|
|
871
|
+
/// would lie to the inline caches.
|
|
872
|
+
fn insert_untracked(&mut self, key: Arc<str>, val: Value) -> Option<Value> {
|
|
873
|
+
if let Some(m) = &mut self.map {
|
|
874
|
+
return m.insert(key, val);
|
|
875
|
+
}
|
|
876
|
+
if let Some(slot) = self.inline.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref()) {
|
|
877
|
+
return Some(core::mem::replace(&mut slot.1, val));
|
|
878
|
+
}
|
|
879
|
+
if self.inline.len() >= PROPMAP_INLINE {
|
|
880
|
+
let mut m: IndexMap<Arc<str>, Value, RandomState> =
|
|
881
|
+
IndexMap::with_capacity_and_hasher(self.inline.len() + 1, RandomState::default());
|
|
882
|
+
for (k, v) in self.inline.drain(..) {
|
|
883
|
+
m.insert(k, v);
|
|
884
|
+
}
|
|
885
|
+
m.insert(key, val);
|
|
886
|
+
self.map = Some(Box::new(m));
|
|
887
|
+
return None;
|
|
888
|
+
}
|
|
889
|
+
self.inline.push((key, val));
|
|
890
|
+
None
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/// Build a PropMap from entries whose ORDER IS NOT MEANINGFUL (a hashed map's iteration
|
|
894
|
+
/// order, e.g. `ObjectMap`/`AHashMap`), as a dictionary-mode object: no shape transitions
|
|
895
|
+
/// are interned and the result carries [`crate::shape::DICT_SHAPE`].
|
|
896
|
+
///
|
|
897
|
+
/// This is a LEAK guard, not just an optimization: `shape::transition` interns a permanent
|
|
898
|
+
/// `ShapeNode` (plus an `Arc` clone of the key) per distinct `(parent, key)` edge, with no
|
|
899
|
+
/// eviction. A hashed source iterates in a DIFFERENT order per instance, so walking
|
|
900
|
+
/// transitions for it mints a fresh permanent chain nearly every call — a builtin returning
|
|
901
|
+
/// such an object from a hot path leaked ~2.3KB per call, unboundedly (multi-GB/hour in a
|
|
902
|
+
/// file watcher calling `stat()` in a loop). Random-order objects can never share a stable
|
|
903
|
+
/// hidden class anyway, so the caches lose nothing: DICT_SHAPE readers take the slow path.
|
|
904
|
+
pub fn from_unordered<I: IntoIterator<Item = (Arc<str>, Value)>>(entries: I) -> Self {
|
|
905
|
+
let mut pm = PropMap::default();
|
|
906
|
+
for (k, v) in entries {
|
|
907
|
+
pm.insert_untracked(k, v);
|
|
908
|
+
}
|
|
909
|
+
pm.shape = crate::shape::DICT_SHAPE;
|
|
910
|
+
pm
|
|
911
|
+
}
|
|
912
|
+
|
|
868
913
|
pub fn remove(&mut self, key: &str) -> Option<Value> {
|
|
869
914
|
let removed = match &mut self.map {
|
|
870
915
|
// shift_remove preserves insertion order (vs swap_remove).
|
|
@@ -1055,8 +1100,11 @@ pub struct ObjectData {
|
|
|
1055
1100
|
impl ObjectData {
|
|
1056
1101
|
#[inline]
|
|
1057
1102
|
pub fn from_strings<I: IntoIterator<Item = (Arc<str>, Value)>>(strings: I) -> Self {
|
|
1103
|
+
// Callers hand us `ObjectMap` (AHashMap) contents — iteration order is random per map
|
|
1104
|
+
// instance, so these become dictionary-mode objects (`DICT_SHAPE`) rather than minting
|
|
1105
|
+
// permanent shape-transition chains per call. See `PropMap::from_unordered`.
|
|
1058
1106
|
Self {
|
|
1059
|
-
strings: strings
|
|
1107
|
+
strings: PropMap::from_unordered(strings),
|
|
1060
1108
|
symbols: None,
|
|
1061
1109
|
frozen: false,
|
|
1062
1110
|
}
|
|
@@ -2019,6 +2067,37 @@ mod number_to_string_tests {
|
|
|
2019
2067
|
}
|
|
2020
2068
|
}
|
|
2021
2069
|
|
|
2070
|
+
#[cfg(test)]
|
|
2071
|
+
mod dict_shape_leak_tests {
|
|
2072
|
+
use super::*;
|
|
2073
|
+
|
|
2074
|
+
/// Objects built from hashed maps (random iteration order) must be dictionary-mode:
|
|
2075
|
+
/// DICT_SHAPE, no per-order shape-transition chains interned in the global registry.
|
|
2076
|
+
/// Regression test for the unbounded shape-registry leak (~2.3KB per builtin object
|
|
2077
|
+
/// built via `Value::object(ObjectMap)` — multi-GB/hour through fs stat() polling).
|
|
2078
|
+
#[test]
|
|
2079
|
+
fn from_strings_is_dict_shape() {
|
|
2080
|
+
let mut m = ObjectMap::default();
|
|
2081
|
+
for i in 0..20 {
|
|
2082
|
+
m.insert(format!("key{i}").into(), Value::Number(i as f64));
|
|
2083
|
+
}
|
|
2084
|
+
let v = Value::object(m);
|
|
2085
|
+
let Value::Object(od) = &v else { panic!("expected object") };
|
|
2086
|
+
assert_eq!(od.borrow().strings.shape(), crate::shape::DICT_SHAPE);
|
|
2087
|
+
// Reads still work through the dictionary path.
|
|
2088
|
+
assert!(matches!(object_get(&v, &Value::String("key7".into())), Some(Value::Number(n)) if n == 7.0));
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
/// from_unordered preserves values and handles the inline→map promotion at >8 keys.
|
|
2092
|
+
#[test]
|
|
2093
|
+
fn from_unordered_promotes_and_reads() {
|
|
2094
|
+
let pm = PropMap::from_unordered((0..12).map(|i| (Arc::<str>::from(format!("k{i}")), Value::Number(i as f64))));
|
|
2095
|
+
assert_eq!(pm.len(), 12);
|
|
2096
|
+
assert!(matches!(pm.get("k11"), Some(Value::Number(n)) if *n == 11.0));
|
|
2097
|
+
assert_eq!(pm.shape(), crate::shape::DICT_SHAPE);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2022
2101
|
#[cfg(test)]
|
|
2023
2102
|
mod cycle_coercion_tests_381 {
|
|
2024
2103
|
//! #381: string-coercing a cyclic array/object must terminate (was a silent thread hang),
|
|
@@ -74,31 +74,40 @@ fn ms_since_epoch(t: std::io::Result<SystemTime>) -> f64 {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/// Build a Node-like `Stats` object from metadata (predicate methods + size + times + mode).
|
|
77
|
+
///
|
|
78
|
+
/// FIXED key order via `object_from_pairs`: building through an `ObjectMap` (AHashMap) iterated
|
|
79
|
+
/// in random per-instance order made every call mint a fresh permanent chain in the global shape
|
|
80
|
+
/// registry (~2.3KB leaked per `stat()` call, unbounded — a file watcher stat-walking a workspace
|
|
81
|
+
/// leaked multi-GB/hour). Fixed-order construction dedupes to one cached shape chain, and skips
|
|
82
|
+
/// the intermediate hashmap entirely.
|
|
77
83
|
fn stats_object(md: &std::fs::Metadata) -> Value {
|
|
78
|
-
let mut m = ObjectMap::default();
|
|
79
84
|
let is_file = md.is_file();
|
|
80
85
|
let is_dir = md.is_dir();
|
|
81
86
|
let is_symlink = md.file_type().is_symlink();
|
|
82
|
-
m.insert("isFile".into(), Value::native(move |_| Value::Bool(is_file)));
|
|
83
|
-
m.insert("isDirectory".into(), Value::native(move |_| Value::Bool(is_dir)));
|
|
84
|
-
m.insert("isSymbolicLink".into(), Value::native(move |_| Value::Bool(is_symlink)));
|
|
85
|
-
m.insert("isBlockDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
86
|
-
m.insert("isCharacterDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
87
|
-
m.insert("isFIFO".into(), Value::native(|_| Value::Bool(false)));
|
|
88
|
-
m.insert("isSocket".into(), Value::native(|_| Value::Bool(false)));
|
|
89
|
-
m.insert("size".into(), Value::Number(md.len() as f64));
|
|
90
|
-
m.insert("mtimeMs".into(), Value::Number(ms_since_epoch(md.modified())));
|
|
91
|
-
m.insert("atimeMs".into(), Value::Number(ms_since_epoch(md.accessed())));
|
|
92
|
-
m.insert("birthtimeMs".into(), Value::Number(ms_since_epoch(md.created())));
|
|
93
87
|
#[cfg(unix)]
|
|
94
|
-
{
|
|
88
|
+
let (mode, uid, gid, ino) = {
|
|
95
89
|
use std::os::unix::fs::MetadataExt;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
(md.mode() as f64, md.uid() as f64, md.gid() as f64, md.ino() as f64)
|
|
91
|
+
};
|
|
92
|
+
#[cfg(not(unix))]
|
|
93
|
+
let (mode, uid, gid, ino) = (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
|
|
94
|
+
Value::object_from_pairs([
|
|
95
|
+
("isFile".into(), Value::native(move |_| Value::Bool(is_file))),
|
|
96
|
+
("isDirectory".into(), Value::native(move |_| Value::Bool(is_dir))),
|
|
97
|
+
("isSymbolicLink".into(), Value::native(move |_| Value::Bool(is_symlink))),
|
|
98
|
+
("isBlockDevice".into(), Value::native(|_| Value::Bool(false))),
|
|
99
|
+
("isCharacterDevice".into(), Value::native(|_| Value::Bool(false))),
|
|
100
|
+
("isFIFO".into(), Value::native(|_| Value::Bool(false))),
|
|
101
|
+
("isSocket".into(), Value::native(|_| Value::Bool(false))),
|
|
102
|
+
("size".into(), Value::Number(md.len() as f64)),
|
|
103
|
+
("mtimeMs".into(), Value::Number(ms_since_epoch(md.modified()))),
|
|
104
|
+
("atimeMs".into(), Value::Number(ms_since_epoch(md.accessed()))),
|
|
105
|
+
("birthtimeMs".into(), Value::Number(ms_since_epoch(md.created()))),
|
|
106
|
+
("mode".into(), Value::Number(mode)),
|
|
107
|
+
("uid".into(), Value::Number(uid)),
|
|
108
|
+
("gid".into(), Value::Number(gid)),
|
|
109
|
+
("ino".into(), Value::Number(ino)),
|
|
110
|
+
])
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
// ── cores ─────────────────────────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|