Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Quoted globs in package.json scripts are now registered as entry points.** A test script using the idiomatic `node --test --import tsx 'src/**/*.test.ts'` form captured its argument as the literal `'src/**/*.test.ts'` with the surrounding single quotes intact, so the compiled entry pattern matched zero files and the test files (and their test-only helper modules) surfaced as false `unused-file` / `unused-export` findings. The script tokenizer now strips a matching pair of surrounding single or double quotes from each token, while leaving tokens with internal quotes (such as `can't`) unchanged. Unquoted globs continue to work as before. (Closes [#841](https://github.com/fallow-rs/fallow/issues/841).)
- **Svelte markup `<script src>` tags no longer surface as unresolved imports.** Svelte treats tags such as `<svelte:head><script src="/some-lib.min.js" async></script></svelte:head>` as runtime HTML, not bundled SFC script modules. Fallow now keeps parsing the `src` metadata for span fidelity but no longer emits synthetic imports for Svelte `script src` references, so root-relative browser assets do not become false `unresolved-imports`. Vue external scripts keep their existing graph edges. Thanks [@codingthat](https://github.com/codingthat) for the report. (Closes [#835](https://github.com/fallow-rs/fallow/issues/835).)
- **Pinia stores auto-imported by `@pinia/nuxt` are now tracked in the module graph.** Fallow now activates a Pinia plugin on `@pinia/nuxt`, scans direct store files in `stores/` and Nuxt 4 `app/stores/`, and maps value exports named `use<Name>Store` to the file that provides them. Pages that call `useUserStore()` without an import now keep `stores/user.ts` reachable and credit the matching export, while sibling helper exports can still report as unused. Nested stores and custom `storesDirs` remain out of scope for the default detector. (Closes [#740](https://github.com/fallow-rs/fallow/issues/740).)
- **TanStack Router `routeFileIgnorePattern` warnings now call out JavaScript regex compatibility instead of treating every unsupported pattern as a user typo.** TanStack accepts JavaScript regular expressions, while fallow validates route ignore patterns with Rust's regex engine before matching route file segments. Patterns that use JavaScript-only syntax such as lookahead are still ignored during analysis so the run can continue, but the warning now names `routeFileIgnorePattern`, points at the source config file when known, and explains that the syntax is unsupported by fallow's Rust regex engine. (Refs [#513](https://github.com/fallow-rs/fallow/issues/513).)
Expand Down
68 changes: 67 additions & 1 deletion crates/core/src/scripts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,27 @@ fn extract_args_for_binary(
(file_args, config_args)
}

/// Strip a matching pair of surrounding single or double quotes from a token.
///
/// Only strips when the token both starts and ends with the same quote character.
/// A token with a single internal quote (e.g. `can't`) is returned unchanged.
fn strip_surrounding_quotes(token: &str) -> &str {
if token.len() >= 2 {
let first = token.as_bytes()[0];
let last = token.as_bytes()[token.len() - 1];
if (first == b'\'' || first == b'"') && first == last {
return &token[1..token.len() - 1];
}
}
token
}

/// Parse a single command segment (after splitting on shell operators).
fn parse_command_segment(segment: &str) -> Option<ScriptCommand> {
let tokens: Vec<&str> = segment.split_whitespace().collect();
let tokens: Vec<&str> = segment
.split_whitespace()
.map(strip_surrounding_quotes)
.collect();
if tokens.is_empty() {
return None;
}
Expand Down Expand Up @@ -1327,6 +1345,54 @@ mod tests {
assert!(!result.used_packages.contains("server"));
}

#[test]
fn node_test_quoted_glob_strips_quotes() {
// Regression test for issue #841: quoted glob args kept their quotes,
// causing looks_like_file_path to reject them and the entry pattern to
// match zero files.
let cmds = parse_script("node --test --import tsx 'src/**/*.test.ts'");
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].binary, "node");
// The surrounding quotes must be stripped from the glob.
assert!(
cmds[0].file_args.contains(&"src/**/*.test.ts".to_string()),
"expected unquoted glob in file_args, got: {:?}",
cmds[0].file_args
);
assert!(
!cmds[0]
.file_args
.iter()
.any(|f| f.starts_with('\'') || f.ends_with('\'')),
"file_args must not contain surrounding single quotes"
);
}

#[test]
fn node_test_unquoted_glob_still_works() {
// Unquoted globs must continue to be extracted correctly.
let cmds = parse_script("node --test src/**/*.test.ts");
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].binary, "node");
assert!(cmds[0].file_args.contains(&"src/**/*.test.ts".to_string()));
}

#[test]
fn token_with_internal_single_quote_unchanged() {
// A token whose quote is internal (not surrounding) must not be mangled.
// Use a file arg that contains an internal apostrophe but is not shell-quoted.
// We exercise strip_surrounding_quotes directly via a known non-file-path
// context: confirm parse_script does not mangle such a token.
assert_eq!(super::strip_surrounding_quotes("can't"), "can't");
assert_eq!(super::strip_surrounding_quotes("'quoted'"), "quoted");
assert_eq!(super::strip_surrounding_quotes("\"quoted\""), "quoted");
assert_eq!(
super::strip_surrounding_quotes("'mismatched\""),
"'mismatched\""
);
assert_eq!(super::strip_surrounding_quotes(""), "");
}

mod proptests {
use super::*;
use proptest::prelude::*;
Expand Down
Loading