@tishlang/tish-lint 3.9.2 → 3.10.3

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 CHANGED
@@ -22,8 +22,6 @@ members = [
22
22
  "crates/tish_vm",
23
23
  "crates/tish_llvm",
24
24
  "crates/tish_ffi",
25
- "examples/ffi/mathext",
26
-
27
25
  "crates/tish_native",
28
26
  "crates/tish_wasm",
29
27
  "crates/tish",
package/bin/tish-lint CHANGED
Binary file
@@ -756,31 +756,60 @@ pub fn compute_native_build_artifacts(
756
756
  })
757
757
  }
758
758
 
759
+ /// Names a directory may declare and still satisfy `package_name`.
760
+ ///
761
+ /// A `tish:foo` import asks for the package `tish-foo`, which is what the in-repo crate
762
+ /// directories under `tish-apple/crates/` are called. The SAME code is published to npm under the
763
+ /// scoped name `@tishlang/tish-foo`, so a project that depends on the published package has it at
764
+ /// `node_modules/@tishlang/tish-foo` declaring that scoped name. Treat the two as one identity,
765
+ /// or a pinned published dependency is silently ignored in favour of a sibling checkout.
766
+ fn acceptable_package_names(package_name: &str) -> Vec<String> {
767
+ let mut names = vec![package_name.to_string()];
768
+ if !package_name.starts_with('@') {
769
+ names.push(format!("@tishlang/{package_name}"));
770
+ }
771
+ names
772
+ }
773
+
774
+ fn declares_one_of(pkg_dir: &Path, names: &[String]) -> bool {
775
+ let pkg_json = pkg_dir.join("package.json");
776
+ if !pkg_json.exists() {
777
+ return false;
778
+ }
779
+ match read_package_name(&pkg_json) {
780
+ Some(found) => names.iter().any(|n| *n == found),
781
+ None => false,
782
+ }
783
+ }
784
+
759
785
  fn find_package_dir(package_name: &str, project_root: &Path) -> Result<PathBuf, String> {
786
+ let names = acceptable_package_names(package_name);
760
787
  let mut search = project_root.to_path_buf();
761
788
  loop {
789
+ // node_modules first, and the scoped published name before the sibling checkout: an
790
+ // explicit dependency pin must beat a directory that merely happens to sit alongside.
762
791
  let node_mod = search.join("node_modules").join(package_name);
763
- if node_mod.join("package.json").exists()
764
- && read_package_name(&node_mod.join("package.json")) == Some(package_name.to_string())
765
- {
792
+ if declares_one_of(&node_mod, &names) {
766
793
  return Ok(node_mod);
767
794
  }
795
+ if !package_name.starts_with('@') {
796
+ let scoped = search
797
+ .join("node_modules")
798
+ .join("@tishlang")
799
+ .join(package_name);
800
+ if declares_one_of(&scoped, &names) {
801
+ return Ok(scoped);
802
+ }
803
+ }
768
804
  let apple_crate = search.join("tish-apple").join("crates").join(package_name);
769
- if apple_crate.join("package.json").exists()
770
- && read_package_name(&apple_crate.join("package.json"))
771
- == Some(package_name.to_string())
772
- {
805
+ if declares_one_of(&apple_crate, &names) {
773
806
  return Ok(apple_crate);
774
807
  }
775
808
  let sibling = search.join(package_name);
776
- if sibling.join("package.json").exists()
777
- && read_package_name(&sibling.join("package.json")) == Some(package_name.to_string())
778
- {
809
+ if declares_one_of(&sibling, &names) {
779
810
  return Ok(sibling);
780
811
  }
781
- if search.join("package.json").exists()
782
- && read_package_name(&search.join("package.json")) == Some(package_name.to_string())
783
- {
812
+ if declares_one_of(&search, &names) {
784
813
  return Ok(search);
785
814
  }
786
815
  if let Some(parent) = search.parent() {
@@ -2385,4 +2414,53 @@ mod find_package_dir_tests {
2385
2414
  let hit = find_package_dir("tish-macos", &project).expect("resolve");
2386
2415
  assert_eq!(hit.canonicalize().unwrap(), apple.canonicalize().unwrap());
2387
2416
  }
2417
+
2418
+ /// The published npm package for `tish:macos` is named `@tishlang/tish-macos`, so a bare
2419
+ /// name-equality check rejects it and silently falls through to a sibling checkout --
2420
+ /// pulling that checkout's path-dep copy of tishlang_core in beside the registry one and
2421
+ /// failing the build on mismatched `Value` types. An explicit dependency must win.
2422
+ #[test]
2423
+ fn prefers_scoped_node_modules_over_tish_apple_crates() {
2424
+ let dir = tempfile::tempdir().expect("tempdir");
2425
+ let root = dir.path();
2426
+ let apple = root.join("tish-apple").join("crates").join("tish-macos");
2427
+ std::fs::create_dir_all(&apple).unwrap();
2428
+ std::fs::write(
2429
+ apple.join("package.json"),
2430
+ r#"{"name":"tish-macos","tish":{"module":true,"crate":"tishlang_macos","export":"macos_object"}}"#,
2431
+ )
2432
+ .unwrap();
2433
+ let project = root.join("app");
2434
+ let scoped = project
2435
+ .join("node_modules")
2436
+ .join("@tishlang")
2437
+ .join("tish-macos");
2438
+ std::fs::create_dir_all(&scoped).unwrap();
2439
+ std::fs::write(
2440
+ scoped.join("package.json"),
2441
+ r#"{"name":"@tishlang/tish-macos","tish":{"module":true,"crate":"tishlang_macos","export":"macos_object"}}"#,
2442
+ )
2443
+ .unwrap();
2444
+ let hit = find_package_dir("tish-macos", &project).expect("resolve");
2445
+ assert_eq!(hit.canonicalize().unwrap(), scoped.canonicalize().unwrap());
2446
+ }
2447
+
2448
+ /// npm alias installs (`"tish-macos": "npm:@tishlang/tish-macos@1.2.0"`) put the scoped
2449
+ /// package at the UNSCOPED directory, so the directory name matches but the declared name
2450
+ /// does not. That must resolve too.
2451
+ #[test]
2452
+ fn accepts_scoped_name_at_unscoped_node_modules_dir() {
2453
+ let dir = tempfile::tempdir().expect("tempdir");
2454
+ let root = dir.path();
2455
+ let project = root.join("app");
2456
+ let aliased = project.join("node_modules").join("tish-macos");
2457
+ std::fs::create_dir_all(&aliased).unwrap();
2458
+ std::fs::write(
2459
+ aliased.join("package.json"),
2460
+ r#"{"name":"@tishlang/tish-macos","tish":{"module":true,"crate":"tishlang_macos","export":"macos_object"}}"#,
2461
+ )
2462
+ .unwrap();
2463
+ let hit = find_package_dir("tish-macos", &project).expect("resolve");
2464
+ assert_eq!(hit.canonicalize().unwrap(), aliased.canonicalize().unwrap());
2465
+ }
2388
2466
  }
@@ -905,6 +905,9 @@ impl Printer {
905
905
  if lead_indent {
906
906
  self.indent(level);
907
907
  }
908
+ if self.try_inline_block(statements, span, level) {
909
+ return;
910
+ }
908
911
  self.buf.push_str("{\n");
909
912
  // Fresh sequence: suppress any blank line immediately after `{`.
910
913
  self.emitted = 0;
@@ -920,6 +923,80 @@ impl Printer {
920
923
  self.emitted = span.start.0;
921
924
  }
922
925
 
926
+ /// True when the source wrote this whole block on one line (`{ … }` with its
927
+ /// matching brace on the same line). The codebase uses `if (c) { return x }`
928
+ /// and `while (c) { a; b }` as a deliberate idiom; exploding those into
929
+ /// multi-line blocks was by far the largest source of reformat churn, so a
930
+ /// block that arrived on one line is kept on one line when it still fits.
931
+ fn source_one_liner(&self, span: Span) -> bool {
932
+ match self.braces.get(&span.start) {
933
+ Some(&(close_line, _)) => close_line == span.start.0,
934
+ None => false,
935
+ }
936
+ }
937
+
938
+ /// Statements simple enough to sit inside a preserved one-liner block. Anything
939
+ /// carrying a block of its own is excluded — nesting one-liners reads worse than
940
+ /// breaking, and the inline printer here emits no newlines or indentation.
941
+ fn inlinable_stmt(s: &Statement) -> bool {
942
+ matches!(
943
+ s,
944
+ Statement::ExprStmt { .. }
945
+ | Statement::Return { .. }
946
+ | Statement::Break { .. }
947
+ | Statement::Continue { .. }
948
+ | Statement::VarDecl { .. }
949
+ | Statement::VarDeclDestructure { .. }
950
+ | Statement::Throw { .. }
951
+ )
952
+ }
953
+
954
+ /// Re-emit a source one-liner block as `{ a; b }`. Returns false (leaving `buf`
955
+ /// untouched) when the block does not qualify or the flat form would overflow
956
+ /// [`WIDTH`], in which case the caller falls through to the normal broken form.
957
+ fn try_inline_block(&mut self, statements: &[Statement], span: Span, _level: usize) -> bool {
958
+ if self.src.is_empty() || statements.is_empty() {
959
+ return false;
960
+ }
961
+ if !self.source_one_liner(span) {
962
+ return false;
963
+ }
964
+ if !statements.iter().all(Self::inlinable_stmt) {
965
+ return false;
966
+ }
967
+ // A comment anywhere inside would be dropped by the inline path, which emits
968
+ // no comments at all. Bail out and let the broken form place it properly.
969
+ let close = self.braces.get(&span.start).copied().unwrap_or(span.start);
970
+ if self
971
+ .comments
972
+ .get(self.ci)
973
+ .is_some_and(|c| c.start >= span.start && c.start < close)
974
+ {
975
+ return false;
976
+ }
977
+
978
+ let mark = self.buf.len();
979
+ let col = self.col();
980
+ let was_flat = self.force_flat;
981
+ self.force_flat = true;
982
+ self.buf.push_str("{ ");
983
+ for (i, st) in statements.iter().enumerate() {
984
+ if i > 0 {
985
+ self.buf.push_str("; ");
986
+ }
987
+ self.stmt(st, 0);
988
+ }
989
+ self.buf.push_str(" }");
990
+ self.force_flat = was_flat;
991
+
992
+ if col + (self.buf.len() - mark) <= WIDTH && !self.buf[mark..].contains('\n') {
993
+ self.emitted = span.start.0;
994
+ return true;
995
+ }
996
+ self.buf.truncate(mark);
997
+ false
998
+ }
999
+
923
1000
  fn stmt_inline_or_block(&mut self, s: &Statement, level: usize) {
924
1001
  if let Statement::Block { statements, span } = s {
925
1002
  self.block(statements, *span, level, false);
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_lint"
3
- version = "3.9.2"
3
+ version = "3.10.3"
4
4
  edition = "2021"
5
5
  description = "AST-based linter for Tish"
6
6
  license-file = { workspace = true }
@@ -3,6 +3,14 @@ name = "tishlang_runtime_gba"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
5
  description = "no_std runtime facade for Tish compiled output on the Game Boy Advance (agb)"
6
+ # Spelled out, NOT `{ workspace = true }`: this crate is in the workspace `exclude` list, so it has
7
+ # no workspace root to inherit from. Without these, `cargo publish` refuses the crate outright,
8
+ # which is why it is the one tishlang_* crate that never shipped.
9
+ #
10
+ # The LICENSE is a COPY, not the root one: cargo requires license-file to resolve inside the
11
+ # package directory, and `../../LICENSE` is not packaged.
12
+ license-file = "LICENSE"
13
+ repository = "https://github.com/tishlang/tish"
6
14
 
7
15
  # The GBA `Gba` emit mode declares the generated crate's dependency as
8
16
  # tishlang_runtime = { package = "tishlang_runtime_gba", path = ... }
@@ -11,6 +19,6 @@ description = "no_std runtime facade for Tish compiled output on the Game Boy Ad
11
19
  # tishlang_core / tishlang_builtins and adds the GBA `gba::{init, halt, hooks}`.
12
20
 
13
21
  [dependencies]
14
- tishlang_core = { path = "../tish_core", default-features = false, features = ["portable"] }
15
- tishlang_builtins = { path = "../tish_builtins", default-features = false, features = ["portable"] }
22
+ tishlang_core = { path = "../tish_core", version = ">=0.1", default-features = false, features = ["portable"] }
23
+ tishlang_builtins = { path = "../tish_builtins", version = ">=0.1", default-features = false, features = ["portable"] }
16
24
  agb = "0.25.0"
@@ -0,0 +1,13 @@
1
+ Pay It Forward License (PIF)
2
+
3
+ Copyright (c) 2025-present The Tish Project Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ This license includes a perpetual, worldwide, non-exclusive, royalty-free, irrevocable (except as stated below) grant for patent claims necessarily infringed by the use of the Software or any contributions submitted to it; terminating automatically when an entity initiates patent litigation (including a cross-claim or counterclaim) alleging that the Software or any portion thereof infringes a patent.
10
+
11
+ The software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
12
+
13
+ Recipients of the Software should pay it forward by contributing to the open-source community through actions including but not limited to providing code, documentation, tutorials, guides, support, feedback, or promoting open-source projects through advocacy or related efforts that advance innovation and community growth.
@@ -1,6 +1,7 @@
1
1
  //! Shared JSX lowering: emit `h(tag, props, children)` as JavaScript or Rust (`Value`) source.
2
2
 
3
3
  use std::collections::HashSet;
4
+ use std::sync::Arc;
4
5
 
5
6
  use tishlang_ast::{
6
7
  ArrayElement, ArrowBody, CallArg, ExportDeclaration, Expr, JsxAttrValue, JsxChild, JsxProp,
@@ -401,6 +402,7 @@ where
401
402
  match expr {
402
403
  Expr::JsxElement {
403
404
  tag,
405
+ tag_span,
404
406
  props,
405
407
  children,
406
408
  ..
@@ -412,7 +414,16 @@ where
412
414
  .unwrap_or(false);
413
415
  let tag_rust = if is_component {
414
416
  if fun_decls.contains(tag.as_ref()) {
415
- escape_ident_rust(tag.as_ref())
417
+ // Lower the component reference through the caller's own identifier path
418
+ // rather than writing the bare name. A module-scope function is promoted to a
419
+ // `__TISH_GF_<name>` static, and there is no local of the original name to
420
+ // reference — emitting it verbatim produced
421
+ // `error[E0425]: cannot find value \`Comp\` in this scope` for every imported
422
+ // component. `emit_expr` already resolves statics, locals and captures.
423
+ emit_expr(&Expr::Ident {
424
+ name: Arc::clone(tag),
425
+ span: *tag_span,
426
+ })?
416
427
  } else {
417
428
  format!("Value::String({:?}.into())", tag.as_ref())
418
429
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-lint",
3
- "version": "3.9.2",
3
+ "version": "3.10.3",
4
4
  "description": "Tish code linter",
5
5
  "license": "PIF",
6
6
  "repository": {
@@ -14,7 +14,9 @@
14
14
  "tish-lint": "./bin/tish-lint"
15
15
  },
16
16
  "scripts": {
17
- "postinstall": "node scripts/install-bin.js"
17
+ "postinstall": "node scripts/install-bin.js",
18
+ "prepack": "node scripts/prepack.js",
19
+ "postpack": "node scripts/postpack.js"
18
20
  },
19
21
  "files": [
20
22
  "bin",
Binary file
Binary file
Binary file
Binary file
Binary file