jscpd-rs 0.1.2 → 0.1.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/CHANGELOG.md +14 -0
- package/Cargo.lock +1 -1
- package/Cargo.toml +2 -1
- package/README.md +28 -4
- package/docs/npm-release.md +29 -25
- package/docs/prebuilt-binaries.md +14 -6
- package/docs/public-benchmark-suite.md +5 -1
- package/docs/release-checklist.md +17 -10
- package/docs/release-decisions.md +7 -1
- package/docs/release-readiness.md +6 -3
- package/package.json +6 -6
- package/src/bin/jscpd-server.rs +1 -1
- package/src/blame.rs +116 -0
- package/src/cli/config.rs +200 -0
- package/src/detector/matching/secondary.rs +0 -1
- package/src/detector/model.rs +4 -3
- package/src/detector/prepare.rs +12 -6
- package/src/detector.rs +25 -19
- package/src/files/discovery.rs +49 -45
- package/src/files/gitignore.rs +80 -0
- package/src/files/shebang.rs +61 -0
- package/src/files/test_support.rs +21 -0
- package/src/files/tests.rs +112 -13
- package/src/files.rs +3 -0
- package/src/formats.rs +216 -212
- package/src/lib.rs +17 -249
- package/src/main.rs +4 -4
- package/src/server.rs +10 -8
- package/src/tokenizer/blocks.rs +20 -24
- package/src/tokenizer/oxc/jsx.rs +77 -0
- package/src/verbose.rs +60 -0
package/src/cli/config.rs
CHANGED
|
@@ -501,6 +501,206 @@ mod tests {
|
|
|
501
501
|
);
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
#[test]
|
|
505
|
+
fn config_number_strings_follow_node_number_rules() {
|
|
506
|
+
assert_eq!(parse_config_usize_string("0x10").unwrap(), 16);
|
|
507
|
+
assert_eq!(parse_config_usize_string(" 42 ").unwrap(), 42);
|
|
508
|
+
assert!(parse_config_usize_string("1.5").is_err());
|
|
509
|
+
assert!(parse_config_usize_string("-1").is_err());
|
|
510
|
+
assert!(parse_config_usize_string("Infinity").is_err());
|
|
511
|
+
|
|
512
|
+
let config: FileConfig =
|
|
513
|
+
serde_json::from_str(r#"{"minLines":"0b101","threshold":"1e2"}"#).unwrap();
|
|
514
|
+
assert_eq!(config.min_lines, Some(5));
|
|
515
|
+
assert_eq!(config.threshold, Some(100.0));
|
|
516
|
+
|
|
517
|
+
let error = serde_json::from_str::<FileConfig>(r#"{"minLines":[]}"#).unwrap_err();
|
|
518
|
+
assert!(error.to_string().contains("invalid type: array"));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#[test]
|
|
522
|
+
fn config_number_deserializers_accept_numbers_nulls_and_reject_wrong_types() {
|
|
523
|
+
let config: FileConfig =
|
|
524
|
+
serde_json::from_str(r#"{"minLines":7,"maxLines":null,"threshold":2.5}"#).unwrap();
|
|
525
|
+
assert_eq!(config.min_lines, Some(7));
|
|
526
|
+
assert_eq!(config.max_lines, None);
|
|
527
|
+
assert_eq!(config.threshold, Some(2.5));
|
|
528
|
+
|
|
529
|
+
let config: FileConfig =
|
|
530
|
+
serde_json::from_str(r#"{"minLines":null,"threshold":null}"#).unwrap();
|
|
531
|
+
assert_eq!(config.min_lines, None);
|
|
532
|
+
assert_eq!(config.threshold, None);
|
|
533
|
+
|
|
534
|
+
let error = serde_json::from_str::<FileConfig>(r#"{"threshold":{}}"#).unwrap_err();
|
|
535
|
+
assert!(error.to_string().contains("invalid type: object"));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#[test]
|
|
539
|
+
fn clean_lexical_path_collapses_dot_and_parent_components() {
|
|
540
|
+
assert_eq!(
|
|
541
|
+
clean_lexical_path(Path::new("repo/app/./src/../tests/file.js")),
|
|
542
|
+
PathBuf::from("repo/app/tests/file.js")
|
|
543
|
+
);
|
|
544
|
+
assert_eq!(
|
|
545
|
+
clean_lexical_path(Path::new("../repo/./src")),
|
|
546
|
+
PathBuf::from("../repo/src")
|
|
547
|
+
);
|
|
548
|
+
assert!(
|
|
549
|
+
absolute_config_path(Path::new("Cargo.toml"))
|
|
550
|
+
.unwrap()
|
|
551
|
+
.is_absolute()
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#[test]
|
|
556
|
+
fn resolves_config_paths_and_ignores_like_upstream() {
|
|
557
|
+
let cwd = std::env::current_dir().unwrap();
|
|
558
|
+
let config_dir = cwd.join("fixtures").join("config");
|
|
559
|
+
let absolute = cwd.join("src");
|
|
560
|
+
|
|
561
|
+
assert_eq!(
|
|
562
|
+
resolve_config_path(&config_dir, "src"),
|
|
563
|
+
config_dir.join("src")
|
|
564
|
+
);
|
|
565
|
+
assert_eq!(resolve_config_path(&config_dir, absolute.clone()), absolute);
|
|
566
|
+
assert_eq!(
|
|
567
|
+
resolve_config_ignore(&config_dir, "**/vendor/**".to_string()).unwrap(),
|
|
568
|
+
"**/vendor/**"
|
|
569
|
+
);
|
|
570
|
+
assert_eq!(
|
|
571
|
+
resolve_config_ignore(&cwd, "target/**".to_string()).unwrap(),
|
|
572
|
+
"target/**"
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
let absolute = cwd.join("dist/**").display().to_string();
|
|
576
|
+
assert_eq!(
|
|
577
|
+
resolve_config_ignore(&config_dir, absolute.clone()).unwrap(),
|
|
578
|
+
absolute
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn applies_config_mappings_and_exit_code_variants() {
|
|
584
|
+
let config: FileConfig = serde_json::from_str(
|
|
585
|
+
r#"{
|
|
586
|
+
"format": "javascript,typescript",
|
|
587
|
+
"formatsExts": {"javascript": ["mjs", "cjs"]},
|
|
588
|
+
"formatsNames": "makefile:Makefile,GNUmakefile",
|
|
589
|
+
"exitCode": false,
|
|
590
|
+
"tokensToSkip": "import,require"
|
|
591
|
+
}"#,
|
|
592
|
+
)
|
|
593
|
+
.unwrap();
|
|
594
|
+
let mut options = Options::default();
|
|
595
|
+
|
|
596
|
+
apply_config(&mut options, config, Path::new(".")).unwrap();
|
|
597
|
+
|
|
598
|
+
assert_eq!(
|
|
599
|
+
options.format_order,
|
|
600
|
+
Some(vec!["javascript".to_string(), "typescript".to_string()])
|
|
601
|
+
);
|
|
602
|
+
assert_eq!(
|
|
603
|
+
options.formats_exts.find_format_for_value("mjs"),
|
|
604
|
+
Some("javascript")
|
|
605
|
+
);
|
|
606
|
+
assert_eq!(
|
|
607
|
+
options.formats_names.find_format_for_value("GNUmakefile"),
|
|
608
|
+
Some("makefile")
|
|
609
|
+
);
|
|
610
|
+
assert_eq!(options.exit_code, ExitCode::Boolean(false));
|
|
611
|
+
assert_eq!(options.tokens_to_skip, vec!["import", "require"]);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#[test]
|
|
615
|
+
fn node_like_json_syntax_message_handles_eof() {
|
|
616
|
+
let data = "{\"minLines\":";
|
|
617
|
+
let error = serde_json::from_str::<FileConfig>(data).unwrap_err();
|
|
618
|
+
|
|
619
|
+
assert_eq!(
|
|
620
|
+
node_like_json_syntax_message(data, &error),
|
|
621
|
+
"Unexpected end of JSON input"
|
|
622
|
+
);
|
|
623
|
+
assert_eq!(json_error_position("a\nbc", 2, 2), 3);
|
|
624
|
+
|
|
625
|
+
let trailing = "[1,]";
|
|
626
|
+
let error = serde_json::from_str::<serde_json::Value>(trailing).unwrap_err();
|
|
627
|
+
assert!(node_like_json_syntax_message(trailing, &error).contains("at position"));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#[test]
|
|
631
|
+
fn read_config_reports_syntax_errors_with_upstream_shape() {
|
|
632
|
+
let root = unique_temp_dir("jscpd-rs-config-syntax");
|
|
633
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
634
|
+
let path = root.join(".jscpd.json");
|
|
635
|
+
std::fs::write(&path, "{ invalid json\n").unwrap();
|
|
636
|
+
|
|
637
|
+
let error = read_config(Some(&path)).unwrap_err();
|
|
638
|
+
let _ = std::fs::remove_dir_all(root);
|
|
639
|
+
|
|
640
|
+
assert!(error.to_string().starts_with("SyntaxError: "));
|
|
641
|
+
assert!(error.to_string().contains("Expected property name or '}'"));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#[test]
|
|
645
|
+
fn apply_config_updates_main_runtime_flags() {
|
|
646
|
+
let config: FileConfig = serde_json::from_str(
|
|
647
|
+
r#"{
|
|
648
|
+
"executionId": "run-1",
|
|
649
|
+
"path": "src,tests",
|
|
650
|
+
"pattern": "**/*.rs",
|
|
651
|
+
"ignore": "target/**,dist/**",
|
|
652
|
+
"reporters": "json,console",
|
|
653
|
+
"ignorePattern": ["import .*"],
|
|
654
|
+
"minLines": 3,
|
|
655
|
+
"minTokens": 7,
|
|
656
|
+
"maxLines": 99,
|
|
657
|
+
"maxSize": "2kb",
|
|
658
|
+
"threshold": 10,
|
|
659
|
+
"mode": "weak",
|
|
660
|
+
"silent": true,
|
|
661
|
+
"absolute": true,
|
|
662
|
+
"noSymlinks": true,
|
|
663
|
+
"ignoreCase": true,
|
|
664
|
+
"gitignore": false,
|
|
665
|
+
"debug": true,
|
|
666
|
+
"verbose": true,
|
|
667
|
+
"skipLocal": true,
|
|
668
|
+
"exitCode": 2,
|
|
669
|
+
"noTips": true
|
|
670
|
+
}"#,
|
|
671
|
+
)
|
|
672
|
+
.unwrap();
|
|
673
|
+
let mut options = Options::default();
|
|
674
|
+
|
|
675
|
+
apply_config(&mut options, config, Path::new(".")).unwrap();
|
|
676
|
+
|
|
677
|
+
assert_eq!(options.execution_id.as_deref(), Some("run-1"));
|
|
678
|
+
assert_eq!(
|
|
679
|
+
options.paths,
|
|
680
|
+
vec![PathBuf::from("./src"), PathBuf::from("./tests")]
|
|
681
|
+
);
|
|
682
|
+
assert_eq!(options.pattern, "**/*.rs");
|
|
683
|
+
assert_eq!(options.ignore, vec!["./target/**", "./dist/**"]);
|
|
684
|
+
assert_eq!(options.reporters, vec!["json", "console"]);
|
|
685
|
+
assert_eq!(options.ignore_pattern.len(), 1);
|
|
686
|
+
assert_eq!(options.min_lines, 3);
|
|
687
|
+
assert_eq!(options.min_tokens, 7);
|
|
688
|
+
assert_eq!(options.max_lines, 99);
|
|
689
|
+
assert_eq!(options.max_size_bytes, 2 * 1024);
|
|
690
|
+
assert_eq!(options.threshold, Some(10.0));
|
|
691
|
+
assert_eq!(options.mode, super::super::Mode::Weak);
|
|
692
|
+
assert!(options.silent);
|
|
693
|
+
assert!(options.absolute);
|
|
694
|
+
assert!(options.no_symlinks);
|
|
695
|
+
assert!(options.ignore_case);
|
|
696
|
+
assert!(!options.gitignore);
|
|
697
|
+
assert!(options.debug);
|
|
698
|
+
assert!(options.verbose);
|
|
699
|
+
assert!(options.skip_local);
|
|
700
|
+
assert_eq!(options.exit_code, ExitCode::Number(2.0));
|
|
701
|
+
assert!(options.no_tips);
|
|
702
|
+
}
|
|
703
|
+
|
|
504
704
|
#[cfg(unix)]
|
|
505
705
|
#[test]
|
|
506
706
|
fn read_config_preserves_symlink_path_like_upstream() {
|
package/src/detector/model.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::collections::{BTreeMap, HashMap};
|
|
2
|
+
use std::sync::Arc;
|
|
2
3
|
|
|
3
4
|
use serde::Serialize;
|
|
4
5
|
|
|
@@ -157,7 +158,6 @@ pub(super) struct TokenSpan {
|
|
|
157
158
|
pub(super) struct SourceMeta {
|
|
158
159
|
pub(super) source_id: String,
|
|
159
160
|
pub(super) format: String,
|
|
160
|
-
pub(super) content: String,
|
|
161
161
|
pub(super) lines: usize,
|
|
162
162
|
pub(super) tokens: usize,
|
|
163
163
|
}
|
|
@@ -185,6 +185,7 @@ pub(super) struct PreparedSource {
|
|
|
185
185
|
#[derive(Clone, Debug)]
|
|
186
186
|
pub(crate) struct PreparedSourceDraft {
|
|
187
187
|
pub(super) meta: SourceMeta,
|
|
188
|
-
pub(super)
|
|
189
|
-
pub(super)
|
|
188
|
+
pub(super) content: Arc<str>,
|
|
189
|
+
pub(super) hashes: Arc<Vec<u64>>,
|
|
190
|
+
pub(super) spans: Arc<Vec<TokenSpan>>,
|
|
190
191
|
}
|
package/src/detector/prepare.rs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
1
3
|
use rustc_hash::FxHashMap;
|
|
2
4
|
|
|
3
5
|
use crate::cli::Options;
|
|
@@ -26,19 +28,23 @@ pub(super) fn assign_formats(files: &[PreparedSourceDraft]) -> (Vec<FormatId>, V
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
pub(super) fn prepare_file_maps(file: SourceFile, options: &Options) -> Vec<PreparedSourceDraft> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
let source_id = file.source_id;
|
|
32
|
+
let content = file.content;
|
|
33
|
+
let maps = tokenize_maps_for_detection(&content, &file.format, options);
|
|
34
|
+
let content = Arc::<str>::from(content);
|
|
35
|
+
|
|
36
|
+
maps.into_iter()
|
|
31
37
|
.map(|map| {
|
|
32
38
|
let (hashes, spans) = split_tokens(map.tokens);
|
|
33
39
|
let (stat_lines, stat_tokens) = token_stream_statistics(&spans);
|
|
34
40
|
PreparedSourceDraft {
|
|
35
41
|
meta: SourceMeta {
|
|
36
|
-
source_id:
|
|
42
|
+
source_id: source_id.clone(),
|
|
37
43
|
format: map.format,
|
|
38
|
-
content: file.content.clone(),
|
|
39
44
|
lines: stat_lines,
|
|
40
45
|
tokens: stat_tokens,
|
|
41
46
|
},
|
|
47
|
+
content: Arc::clone(&content),
|
|
42
48
|
hashes,
|
|
43
49
|
spans,
|
|
44
50
|
}
|
|
@@ -46,7 +52,7 @@ pub(super) fn prepare_file_maps(file: SourceFile, options: &Options) -> Vec<Prep
|
|
|
46
52
|
.collect()
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
fn split_tokens(tokens: Vec<DetectionToken>) -> (Vec<u64
|
|
55
|
+
fn split_tokens(tokens: Vec<DetectionToken>) -> (Arc<Vec<u64>>, Arc<Vec<TokenSpan>>) {
|
|
50
56
|
let mut hashes = Vec::with_capacity(tokens.len());
|
|
51
57
|
let mut spans = Vec::with_capacity(tokens.len());
|
|
52
58
|
for token in tokens {
|
|
@@ -57,7 +63,7 @@ fn split_tokens(tokens: Vec<DetectionToken>) -> (Vec<u64>, Vec<TokenSpan>) {
|
|
|
57
63
|
range: token.range,
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
|
-
(hashes, spans)
|
|
66
|
+
(Arc::new(hashes), Arc::new(spans))
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
fn token_stream_statistics(spans: &[TokenSpan]) -> (usize, usize) {
|
package/src/detector.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::collections::HashMap;
|
|
2
|
+
use std::sync::Arc;
|
|
2
3
|
|
|
3
4
|
use rayon::prelude::*;
|
|
4
5
|
use rustc_hash::FxHashSet;
|
|
@@ -131,29 +132,36 @@ pub(crate) fn detect_prepared_drafts(
|
|
|
131
132
|
prepared_drafts: Vec<PreparedSourceDraft>,
|
|
132
133
|
options: &Options,
|
|
133
134
|
) -> DetectionResult {
|
|
135
|
+
let include_source_contents = options
|
|
136
|
+
.reporters
|
|
137
|
+
.iter()
|
|
138
|
+
.any(|reporter| matches!(reporter.as_str(), "json" | "xml" | "html" | "consoleFull"));
|
|
139
|
+
let mut source_contents = HashMap::new();
|
|
134
140
|
let (format_ids, format_names) = assign_formats(&prepared_drafts);
|
|
135
141
|
let prepared_files = prepared_drafts
|
|
136
142
|
.into_iter()
|
|
137
143
|
.enumerate()
|
|
138
|
-
.map(|(idx, draft)|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
.map(|(idx, draft)| {
|
|
145
|
+
if include_source_contents && !draft.spans.is_empty() {
|
|
146
|
+
source_contents
|
|
147
|
+
.entry(draft.meta.source_id.clone())
|
|
148
|
+
.or_insert_with(|| draft.content.to_string());
|
|
149
|
+
}
|
|
150
|
+
PreparedSource {
|
|
151
|
+
meta: draft.meta,
|
|
152
|
+
stream: TokenStream {
|
|
153
|
+
source_id: SourceId(idx),
|
|
154
|
+
format_id: format_ids[idx],
|
|
155
|
+
hashes: unwrap_or_clone_arc_vec(draft.hashes),
|
|
156
|
+
spans: unwrap_or_clone_arc_vec(draft.spans),
|
|
157
|
+
},
|
|
158
|
+
}
|
|
146
159
|
})
|
|
147
160
|
.collect::<Vec<_>>();
|
|
148
161
|
|
|
149
162
|
let mut statistics = Statistics::default();
|
|
150
163
|
let mut sources = Vec::new();
|
|
151
|
-
let mut source_contents = HashMap::new();
|
|
152
164
|
let mut source_indices_by_format = vec![Vec::new(); format_names.len()];
|
|
153
|
-
let include_source_contents = options
|
|
154
|
-
.reporters
|
|
155
|
-
.iter()
|
|
156
|
-
.any(|reporter| matches!(reporter.as_str(), "json" | "xml" | "html" | "consoleFull"));
|
|
157
165
|
|
|
158
166
|
for (idx, prepared) in prepared_files.iter().enumerate() {
|
|
159
167
|
if prepared.stream.spans.is_empty() {
|
|
@@ -172,12 +180,6 @@ pub(crate) fn detect_prepared_drafts(
|
|
|
172
180
|
lines: prepared.meta.lines,
|
|
173
181
|
tokens: prepared.meta.tokens,
|
|
174
182
|
});
|
|
175
|
-
if include_source_contents {
|
|
176
|
-
source_contents.insert(
|
|
177
|
-
prepared.meta.source_id.clone(),
|
|
178
|
-
prepared.meta.content.clone(),
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
183
|
source_indices_by_format[prepared.stream.format_id.0].push(idx);
|
|
182
184
|
}
|
|
183
185
|
|
|
@@ -222,6 +224,10 @@ fn dedup_exact_clones(clones: &mut Vec<CloneMatch>) {
|
|
|
222
224
|
clones.retain(|clone| seen.insert(CloneDedupKey::from(clone)));
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
fn unwrap_or_clone_arc_vec<T: Clone>(value: Arc<Vec<T>>) -> Vec<T> {
|
|
228
|
+
Arc::try_unwrap(value).unwrap_or_else(|value| (*value).clone())
|
|
229
|
+
}
|
|
230
|
+
|
|
225
231
|
#[derive(Hash, Eq, PartialEq)]
|
|
226
232
|
struct CloneDedupKey {
|
|
227
233
|
format: String,
|
package/src/files/discovery.rs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
use std::fs;
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
|
-
use std::sync::{
|
|
3
|
+
use std::sync::{
|
|
4
|
+
Arc,
|
|
5
|
+
atomic::{AtomicBool, Ordering},
|
|
6
|
+
mpsc,
|
|
7
|
+
};
|
|
4
8
|
|
|
5
9
|
use anyhow::{Context, Result, anyhow};
|
|
6
10
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
@@ -59,14 +63,11 @@ pub fn discover(options: &Options) -> Result<Vec<SourceFile>> {
|
|
|
59
63
|
let metadata = fs::metadata(root)
|
|
60
64
|
.with_context(|| format!("failed to inspect path `{}`", root.display()))?;
|
|
61
65
|
if metadata.is_file() {
|
|
62
|
-
|
|
63
|
-
root,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
&cwd,
|
|
68
|
-
&mut candidates,
|
|
69
|
-
)?;
|
|
66
|
+
if let Some(candidate) =
|
|
67
|
+
candidate_for_path(root, root_index, options, &ignore_set, &cwd)?
|
|
68
|
+
{
|
|
69
|
+
candidates.push(candidate);
|
|
70
|
+
}
|
|
70
71
|
continue;
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -158,14 +159,15 @@ fn collect_candidates_sequential(
|
|
|
158
159
|
if !context.pattern_set.is_match(relative) {
|
|
159
160
|
continue;
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
+
if let Some(candidate) = candidate_for_path(
|
|
162
163
|
path,
|
|
163
164
|
context.root_index,
|
|
164
165
|
context.options,
|
|
165
166
|
context.ignore_set,
|
|
166
167
|
context.cwd,
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
)? {
|
|
169
|
+
candidates.push(candidate);
|
|
170
|
+
}
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
Ok(())
|
|
@@ -176,24 +178,25 @@ fn collect_candidates_parallel(
|
|
|
176
178
|
context: &CandidateCollectionContext<'_>,
|
|
177
179
|
candidates: &mut Vec<CandidateFile>,
|
|
178
180
|
) -> Result<()> {
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
+
let (sender, receiver) = mpsc::channel::<Result<CandidateFile>>();
|
|
182
|
+
let failed = Arc::new(AtomicBool::new(false));
|
|
181
183
|
|
|
182
184
|
builder.build_parallel().run(|| {
|
|
183
|
-
let
|
|
184
|
-
let
|
|
185
|
+
let sender = sender.clone();
|
|
186
|
+
let failed = Arc::clone(&failed);
|
|
185
187
|
Box::new(move |entry| {
|
|
186
|
-
if
|
|
188
|
+
if failed.load(Ordering::Relaxed) {
|
|
187
189
|
return WalkState::Quit;
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
let entry = match entry {
|
|
191
193
|
Ok(entry) => entry,
|
|
192
194
|
Err(err) => {
|
|
193
|
-
|
|
195
|
+
failed.store(true, Ordering::Relaxed);
|
|
196
|
+
let _ = sender.send(Err(anyhow!(
|
|
194
197
|
"failed to walk path `{}`: {err}",
|
|
195
198
|
context.root.display()
|
|
196
|
-
));
|
|
199
|
+
)));
|
|
197
200
|
return WalkState::Quit;
|
|
198
201
|
}
|
|
199
202
|
};
|
|
@@ -210,47 +213,50 @@ fn collect_candidates_parallel(
|
|
|
210
213
|
return WalkState::Continue;
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
if let Err(err) = collect_candidate(
|
|
216
|
+
match candidate_for_path(
|
|
215
217
|
path,
|
|
216
218
|
context.root_index,
|
|
217
219
|
context.options,
|
|
218
220
|
context.ignore_set,
|
|
219
221
|
context.cwd,
|
|
220
|
-
&mut local,
|
|
221
222
|
) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
Ok(Some(candidate)) => {
|
|
224
|
+
if sender.send(Ok(candidate)).is_err() {
|
|
225
|
+
failed.store(true, Ordering::Relaxed);
|
|
226
|
+
return WalkState::Quit;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
Ok(None) => {}
|
|
230
|
+
Err(err) => {
|
|
231
|
+
failed.store(true, Ordering::Relaxed);
|
|
232
|
+
let _ = sender.send(Err(err));
|
|
233
|
+
return WalkState::Quit;
|
|
234
|
+
}
|
|
227
235
|
}
|
|
228
236
|
WalkState::Continue
|
|
229
237
|
})
|
|
230
238
|
});
|
|
239
|
+
drop(sender);
|
|
231
240
|
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
for item in receiver {
|
|
242
|
+
candidates.push(item?);
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
candidates.extend(Arc::try_unwrap(collected).unwrap().into_inner().unwrap());
|
|
237
|
-
|
|
238
245
|
Ok(())
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
fn
|
|
248
|
+
fn candidate_for_path(
|
|
242
249
|
path: &Path,
|
|
243
250
|
root_index: usize,
|
|
244
251
|
options: &Options,
|
|
245
252
|
ignore_set: &IgnoreMatcher,
|
|
246
253
|
cwd: &Path,
|
|
247
|
-
|
|
248
|
-
) -> Result<()> {
|
|
254
|
+
) -> Result<Option<CandidateFile>> {
|
|
249
255
|
if options.no_symlinks && is_symlink(path) {
|
|
250
|
-
return Ok(
|
|
256
|
+
return Ok(None);
|
|
251
257
|
}
|
|
252
258
|
if is_ignored(path, ignore_set, cwd) {
|
|
253
|
-
return Ok(
|
|
259
|
+
return Ok(None);
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
let format = if let Some(format) =
|
|
@@ -269,7 +275,7 @@ fn collect_candidate(
|
|
|
269
275
|
options.max_size_bytes
|
|
270
276
|
);
|
|
271
277
|
}
|
|
272
|
-
return Ok(
|
|
278
|
+
return Ok(None);
|
|
273
279
|
}
|
|
274
280
|
shebang_format_for_path(path, &metadata)?.map(str::to_string)
|
|
275
281
|
};
|
|
@@ -277,7 +283,7 @@ fn collect_candidate(
|
|
|
277
283
|
if options.verbose {
|
|
278
284
|
eprintln!("skipped unsupported format: {}", path.display());
|
|
279
285
|
}
|
|
280
|
-
return Ok(
|
|
286
|
+
return Ok(None);
|
|
281
287
|
};
|
|
282
288
|
if let Some(formats) = &options.formats
|
|
283
289
|
&& !formats.contains(format.as_str())
|
|
@@ -285,15 +291,13 @@ fn collect_candidate(
|
|
|
285
291
|
if options.verbose || options.debug {
|
|
286
292
|
println!("{}", format_filter_skip_message(path, &format, cwd));
|
|
287
293
|
}
|
|
288
|
-
return Ok(
|
|
294
|
+
return Ok(None);
|
|
289
295
|
}
|
|
290
|
-
|
|
296
|
+
Ok(Some(CandidateFile {
|
|
291
297
|
path: path.to_path_buf(),
|
|
292
298
|
format,
|
|
293
299
|
root_index,
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
Ok(())
|
|
300
|
+
}))
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
fn read_candidate(
|
|
@@ -415,7 +419,7 @@ fn push_pattern_once(
|
|
|
415
419
|
}
|
|
416
420
|
}
|
|
417
421
|
|
|
418
|
-
fn normalize_glob_path(path: PathBuf) -> String {
|
|
422
|
+
pub(super) fn normalize_glob_path(path: PathBuf) -> String {
|
|
419
423
|
let mut normalized = PathBuf::new();
|
|
420
424
|
for component in path.components() {
|
|
421
425
|
match component {
|
|
@@ -496,7 +500,7 @@ pub(super) fn build_ignore_matcher(patterns: &[String]) -> Result<IgnoreMatcher>
|
|
|
496
500
|
})
|
|
497
501
|
}
|
|
498
502
|
|
|
499
|
-
fn build_glob_set(patterns: &[String]) -> Result<GlobSet> {
|
|
503
|
+
pub(super) fn build_glob_set(patterns: &[String]) -> Result<GlobSet> {
|
|
500
504
|
let mut builder = GlobSetBuilder::new();
|
|
501
505
|
if patterns.is_empty() {
|
|
502
506
|
return Ok(builder.build()?);
|
package/src/files/gitignore.rs
CHANGED
|
@@ -201,3 +201,83 @@ fn push_gitignore_glob_variants(globs: &mut Vec<String>, path: &Path) {
|
|
|
201
201
|
globs.push(format!("{relative}/**"));
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
#[cfg(test)]
|
|
206
|
+
mod tests {
|
|
207
|
+
use super::super::test_support::unique_temp_path;
|
|
208
|
+
use super::*;
|
|
209
|
+
|
|
210
|
+
#[test]
|
|
211
|
+
fn scoped_gitignore_globs_cover_rooted_nested_and_filename_patterns() {
|
|
212
|
+
let base = std::env::current_dir().unwrap();
|
|
213
|
+
|
|
214
|
+
let rooted = scoped_gitignore_globs(&base, "dist", true);
|
|
215
|
+
assert!(rooted.iter().any(|glob| glob == "dist"));
|
|
216
|
+
assert!(rooted.iter().any(|glob| glob == "dist/**"));
|
|
217
|
+
|
|
218
|
+
let nested = scoped_gitignore_globs(&base, "src/generated", false);
|
|
219
|
+
assert!(nested.iter().any(|glob| glob == "src/generated"));
|
|
220
|
+
assert!(nested.iter().any(|glob| glob == "**/src/generated"));
|
|
221
|
+
|
|
222
|
+
let filename = scoped_gitignore_globs(&base, "*.snap", false);
|
|
223
|
+
assert!(filename.iter().any(|glob| glob == "**/*.snap"));
|
|
224
|
+
assert!(filename.iter().any(|glob| glob == "**/*.snap/**"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn gitignore_lines_trim_slashes_backslashes_and_negations() {
|
|
229
|
+
assert_eq!(
|
|
230
|
+
gitignore_line_to_globs(r"build\generated", None),
|
|
231
|
+
vec![
|
|
232
|
+
"build/generated",
|
|
233
|
+
"build/generated/**",
|
|
234
|
+
"**/build/generated",
|
|
235
|
+
"**/build/generated/**",
|
|
236
|
+
]
|
|
237
|
+
);
|
|
238
|
+
assert_eq!(
|
|
239
|
+
gitignore_line_to_globs("!/dist/", None),
|
|
240
|
+
vec!["!dist", "!dist/**"]
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn collect_gitignore_patterns_walks_parents_and_repo_exclude_once() {
|
|
246
|
+
let repo = unique_temp_path("gitignore-repo");
|
|
247
|
+
let nested = repo.join("packages").join("app");
|
|
248
|
+
std::fs::create_dir_all(repo.join(".git").join("info")).unwrap();
|
|
249
|
+
std::fs::create_dir_all(&nested).unwrap();
|
|
250
|
+
std::fs::write(repo.join(".gitignore"), "/target\n").unwrap();
|
|
251
|
+
std::fs::write(nested.join(".gitignore"), "local-cache\n").unwrap();
|
|
252
|
+
std::fs::write(
|
|
253
|
+
repo.join(".git").join("info").join("exclude"),
|
|
254
|
+
"repo-only\n",
|
|
255
|
+
)
|
|
256
|
+
.unwrap();
|
|
257
|
+
|
|
258
|
+
let patterns =
|
|
259
|
+
collect_gitignore_patterns_with_global(&[nested.clone(), nested.join("src")], None);
|
|
260
|
+
let _ = std::fs::remove_dir_all(&repo);
|
|
261
|
+
|
|
262
|
+
assert!(patterns.iter().any(|pattern| pattern.ends_with("/target")));
|
|
263
|
+
assert!(
|
|
264
|
+
patterns
|
|
265
|
+
.iter()
|
|
266
|
+
.any(|pattern| pattern.ends_with("/**/local-cache"))
|
|
267
|
+
);
|
|
268
|
+
assert!(
|
|
269
|
+
patterns
|
|
270
|
+
.iter()
|
|
271
|
+
.any(|pattern| pattern.ends_with("/**/repo-only"))
|
|
272
|
+
);
|
|
273
|
+
let repo_only = patterns
|
|
274
|
+
.iter()
|
|
275
|
+
.filter(|pattern| pattern.contains("repo-only"))
|
|
276
|
+
.collect::<Vec<_>>();
|
|
277
|
+
let unique_repo_only = repo_only
|
|
278
|
+
.iter()
|
|
279
|
+
.copied()
|
|
280
|
+
.collect::<std::collections::HashSet<_>>();
|
|
281
|
+
assert_eq!(repo_only.len(), unique_repo_only.len());
|
|
282
|
+
}
|
|
283
|
+
}
|