1use std::path::Path;
2
3use provable_contracts::lint::config::{find_config, load_config};
4use provable_contracts::lint::rules::RuleSeverity;
5use provable_contracts::lint::shapes_gate::ShapesOptions;
6use provable_contracts::lint::trend;
7use provable_contracts::lint::{run_lint, GateDetail, LintConfig, LintReport};
8use provable_contracts::ontology::verdict::Verdict;
9
10use crate::contract_walk::{LintDeclined, LintRejected, ZeroContracts};
11
12#[path = "lint_render.rs"]
13mod lint_render;
14
15#[path = "lint_html.rs"]
16mod lint_html;
17
18#[path = "lint_arming.rs"]
19mod lint_arming;
20
21pub fn explain_rule(rule_id: &str) {
23 lint_render::print_explain(rule_id);
24}
25
26#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
27pub fn run(
28 contract_dir: &Path,
29 binding_path: Option<&Path>,
30 min_score: f64,
31 format: Option<&str>,
32 severity: Option<&str>,
33 strict: bool,
34 suppress: Option<&str>,
35 suppress_rule: Option<&str>,
36 suppress_file: Option<&str>,
37 rule_overrides: &[String],
38 config_path: Option<&Path>,
39 diff_ref: Option<&str>,
40 do_trend: bool,
41 show_trend: bool,
42 no_cache: bool,
43 cache_stats: bool,
44 coverage: bool,
45 min_coverage: Option<f64>,
46 crate_dir: Option<&Path>,
47 min_level: Option<&str>,
48 watch: bool,
49 strict_test_binding: bool,
50 armed_baseline_ref: Option<&str>,
51 gate: Option<&str>,
52 shapes_opts: ShapesOptions,
53) -> Result<(), Box<dyn std::error::Error>> {
54 refuse_missing_corpus(contract_dir)?;
55 refuse_single_file_strict_binding(contract_dir, strict_test_binding)?;
60 if let Some(name) = gate {
61 return run_single_gate(contract_dir, name, &shapes_opts);
62 }
63 if watch {
64 return run_watch(
65 contract_dir,
66 binding_path,
67 min_score,
68 format,
69 severity,
70 strict,
71 suppress,
72 suppress_rule,
73 suppress_file,
74 rule_overrides,
75 config_path,
76 no_cache,
77 cache_stats,
78 crate_dir,
79 min_level,
80 strict_test_binding,
81 );
82 }
83
84 if show_trend {
85 show_trend_history(contract_dir);
86 return Ok(());
87 }
88
89 if let Some(base) = diff_ref {
90 if let Some(result) = run_diff_check(contract_dir, base) {
91 return result;
92 }
93 }
94
95 let config = build_config(
96 contract_dir,
97 binding_path,
98 min_score,
99 format,
100 severity,
101 strict,
102 suppress,
103 suppress_rule,
104 suppress_file,
105 rule_overrides,
106 config_path,
107 no_cache,
108 cache_stats,
109 crate_dir,
110 min_level,
111 strict_test_binding,
112 );
113
114 let arming = lint_arming::resolve(contract_dir, armed_baseline_ref)?;
116
117 let mut report = run_lint(&config);
118
119 refuse_empty_corpus(&report, contract_dir)?;
122
123 report.arm(&arming.armed);
124 report.armed_monotone = Some(arming.monotone);
125 report.armed_shapes_monotone = Some(arming.shapes_monotone);
126
127 if cache_stats {
128 print_cache_stats(&report);
129 }
130 if do_trend {
131 record_trend(contract_dir, &report);
132 }
133
134 let effective_format = resolve_format(format, config_path, contract_dir);
135 print_report(&effective_format, &report)?;
136
137 if coverage {
139 report_coverage(contract_dir, min_coverage)?;
140 }
141
142 meet_exit(&report)
143}
144
145fn report_coverage(
147 contract_dir: &Path,
148 min_coverage: Option<f64>,
149) -> Result<(), Box<dyn std::error::Error>> {
150 let coverage_result = compute_contract_coverage(contract_dir);
151 println!(
152 "\nContract Coverage: {}/{} at Standard+ ({:.1}%)",
153 coverage_result.standard_plus, coverage_result.total, coverage_result.percentage,
154 );
155 match min_coverage {
156 Some(threshold) if coverage_result.percentage < threshold => Err(format!(
157 "contract coverage {:.1}% is below minimum {:.1}%",
158 coverage_result.percentage, threshold,
159 )
160 .into()),
161 _ => Ok(()),
162 }
163}
164
165pub fn shapes_options(
169 gate: Option<&str>,
170 shape: Option<String>,
171 release: &crate::cli::ReleaseArgs,
172) -> Result<ShapesOptions, crate::contract_walk::ReleaseArgsRefused> {
173 use crate::contract_walk::ReleaseArgsRefused;
174 if (shape.is_some() || release.any()) && gate != Some("shapes") {
175 return Err(ReleaseArgsRefused(
176 "--shape and --release-* / --receipts* / --kernel-receipts / --dogfood-receipt apply only to \
177 `--gate shapes`"
178 .into(),
179 ));
180 }
181 Ok(ShapesOptions {
182 only: shape,
183 release: release.subject().map_err(ReleaseArgsRefused)?,
184 })
185}
186
187#[derive(serde::Serialize)]
190struct SingleGateReport<'a> {
191 gate: &'a str,
192 verdict: Verdict,
193 passed: bool,
194 duration_ms: u64,
195 extra: Option<&'a provable_contracts::lint::GateExtra>,
196 #[serde(flatten)]
200 flat: Option<&'a provable_contracts::lint::GateExtra>,
201 findings: Vec<SingleGateFinding<'a>>,
202}
203
204#[derive(serde::Serialize)]
205struct SingleGateFinding<'a> {
206 rule_id: &'a str,
207 severity: String,
208 message: &'a str,
209 file: &'a str,
210}
211
212fn run_single_gate(
215 contract_dir: &Path,
216 name: &str,
217 shapes_opts: &ShapesOptions,
218) -> Result<(), Box<dyn std::error::Error>> {
219 let (result, findings) = decide_named_gate(contract_dir, name, shapes_opts)?;
220
221 let report = SingleGateReport {
222 gate: &result.name,
223 verdict: result.verdict,
224 passed: result.passed,
225 duration_ms: result.duration_ms,
226 extra: result.extra.as_ref(),
227 flat: result.extra.as_ref(),
228 findings: findings
229 .iter()
230 .map(|f| SingleGateFinding {
231 rule_id: &f.rule_id,
232 severity: format!("{:?}", f.severity),
233 message: &f.message,
234 file: &f.file,
235 })
236 .collect(),
237 };
238 println!("{}", serde_json::to_string_pretty(&report)?);
239
240 match result.verdict {
243 provable_contracts::ontology::verdict::Verdict::Pass => Ok(()),
244 provable_contracts::ontology::verdict::Verdict::Unknown(reason) => {
245 Err(LintDeclined { reason }.into())
246 }
247 provable_contracts::ontology::verdict::Verdict::Fail => Err(LintRejected {
248 passed: 0,
249 armed: 1,
250 }
251 .into()),
252 }
253}
254
255type NamedGateAnswer = (
259 Box<provable_contracts::lint::GateResult>,
260 Vec<provable_contracts::lint::finding::LintFinding>,
261);
262
263fn decide_named_gate(
264 contract_dir: &Path,
265 name: &str,
266 shapes_opts: &ShapesOptions,
267) -> Result<NamedGateAnswer, Box<dyn std::error::Error>> {
268 use provable_contracts::lint::{
269 relations_gate::RelationsOutcome, sigma_gate::SigmaOutcome, NamedGateOutcome, NAMED_GATES,
270 };
271
272 match provable_contracts::lint::run_named_gate_with(contract_dir, name, shapes_opts) {
273 NamedGateOutcome::UnknownGate => Err(crate::contract_walk::UnknownGate {
274 asked: name.to_string(),
275 known: NAMED_GATES.iter().map(|g| (*g).to_string()).collect(),
276 }
277 .into()),
278 NamedGateOutcome::Sigma(SigmaOutcome::NoSigma) => Err(LintDeclined {
279 reason: provable_contracts::ontology::verdict::Reason::NoCheckable,
280 }
281 .into()),
282 NamedGateOutcome::Sigma(SigmaOutcome::Malformed(e)) => {
283 Err(crate::contract_walk::SigmaMalformed(e.to_string()).into())
284 }
285 NamedGateOutcome::Relations(
286 RelationsOutcome::NoSigma | RelationsOutcome::NoRelations { .. },
287 ) => Err(LintDeclined {
288 reason: provable_contracts::ontology::verdict::Reason::NoCheckable,
289 }
290 .into()),
291 NamedGateOutcome::Relations(RelationsOutcome::Malformed(e)) => {
292 Err(crate::contract_walk::SigmaMalformed(e.to_string()).into())
293 }
294 NamedGateOutcome::Shapes(outcome) => decide_shapes_gate(outcome),
295 NamedGateOutcome::Sigma(SigmaOutcome::Ran { result, findings })
296 | NamedGateOutcome::Relations(RelationsOutcome::Ran { result, findings })
297 | NamedGateOutcome::Ran { result, findings } => Ok((result, findings)),
298 }
299}
300
301fn decide_shapes_gate(
304 outcome: provable_contracts::lint::shapes_gate::ShapesOutcome,
305) -> Result<NamedGateAnswer, Box<dyn std::error::Error>> {
306 use provable_contracts::lint::shapes_gate::ShapesOutcome;
307 use provable_contracts::ontology::verdict::Reason;
308
309 match outcome {
310 ShapesOutcome::Unsupported(e) => {
311 Err(crate::contract_walk::SigmaMalformed(e.to_string()).into())
312 }
313 ShapesOutcome::ExtractFailed(e) => {
314 Err(crate::contract_walk::SigmaMalformed(e.to_string()).into())
315 }
316 ShapesOutcome::NoShapes { .. } => Err(LintDeclined {
317 reason: Reason::NoShapes,
318 }
319 .into()),
320 ShapesOutcome::NoFocus { .. } => Err(LintDeclined {
321 reason: Reason::NoFocus,
322 }
323 .into()),
324 ShapesOutcome::WrongCorpus {
325 shapes_n,
326 expected,
327 found,
328 refused,
329 } => {
330 eprintln!(
331 "shapes: extract:parity-receipt matched {found} focus node(s); evidence/parity/EXPECTED_RECEIPTS says {expected} ({shapes_n} shape(s))"
332 );
333 for r in &refused {
334 eprintln!("shapes: refused {r}");
335 }
336 Err(LintDeclined {
337 reason: Reason::WrongCorpus,
338 }
339 .into())
340 }
341 ShapesOutcome::NoReceipts { shapes_n, dir } => {
342 eprintln!(
345 "shapes: {shapes_n} shape(s) resolve receipts and the tree holds none under {dir}/"
346 );
347 Err(LintDeclined {
348 reason: Reason::NoCheckable,
349 }
350 .into())
351 }
352 ShapesOutcome::PositiveControlFailed { which, .. } => {
353 eprintln!("shapes: positive control {which} did not fire");
354 Err(LintDeclined {
355 reason: Reason::PositiveControlFailed,
356 }
357 .into())
358 }
359 ShapesOutcome::Differential {
360 passed, n, failed, ..
361 } => {
362 eprintln!(
364 "shapes: W3C SHACL-Core differential — {passed} of {n} vendored case(s) pass"
365 );
366 for f in &failed {
367 eprintln!(" {f}");
368 }
369 Err(LintDeclined {
370 reason: Reason::Differential,
371 }
372 .into())
373 }
374 ShapesOutcome::Ran { result, findings } => Ok((result, findings)),
375 }
376}
377
378fn meet_exit(report: &LintReport) -> Result<(), Box<dyn std::error::Error>> {
381 match report.verdict {
382 Verdict::Pass => Ok(()),
383 Verdict::Fail => Err(LintRejected {
384 passed: report
385 .armed_gates
386 .iter()
387 .filter(|g| g.verdict == Verdict::Pass)
388 .count(),
389 armed: report.armed_gates.len(),
390 }
391 .into()),
392 Verdict::Unknown(reason) => Err(LintDeclined { reason }.into()),
393 }
394}
395
396fn refuse_empty_corpus(report: &LintReport, contract_dir: &Path) -> Result<(), ZeroContracts> {
404 let empty = report.gates.iter().any(|g| {
405 g.name == "validate"
406 && matches!(
407 g.detail,
408 GateDetail::Validate {
409 contracts: 0,
410 errors: 0,
411 ..
412 }
413 )
414 });
415 if empty {
416 return Err(ZeroContracts {
417 path: contract_dir.to_path_buf(),
418 filter: None,
419 });
420 }
421 Ok(())
422}
423
424struct CoverageResult {
425 standard_plus: usize,
426 total: usize,
427 percentage: f64,
428}
429
430fn compute_contract_coverage(contract_dir: &Path) -> CoverageResult {
432 let mut total = 0usize;
433 let mut standard_plus = 0usize;
434
435 let mut yaml_paths = Vec::new();
436 collect_yaml_files_lint(contract_dir, &mut yaml_paths);
437
438 for path in &yaml_paths {
439 let Ok(contract) = provable_contracts::schema::parse_contract(path) else {
440 continue;
441 };
442 if !contract.requires_proofs() {
445 continue;
446 }
447 total += 1;
448 if !contract.falsification_tests.is_empty() && !contract.kani_harnesses.is_empty() {
449 standard_plus += 1;
450 }
451 }
452
453 #[allow(clippy::cast_precision_loss)]
454 let percentage = if total > 0 {
455 (standard_plus as f64 / total as f64) * 100.0
456 } else {
457 100.0
458 };
459
460 CoverageResult {
461 standard_plus,
462 total,
463 percentage,
464 }
465}
466
467fn show_trend_history(contract_dir: &Path) {
468 let trend_root = trend::trend_dir(contract_dir);
469 let snapshots = trend::load_snapshots(&trend_root);
470 if snapshots.is_empty() {
471 println!("No trend data. Run `pv lint --trend` to record snapshots.");
472 } else {
473 println!("{}", trend::format_trend(&snapshots, 30));
474 }
475}
476
477fn refuse_single_file_strict_binding(
515 path: &Path,
516 strict_test_binding: bool,
517) -> Result<(), Box<dyn std::error::Error>> {
518 if !strict_test_binding || !path.is_file() {
519 return Ok(());
520 }
521 let dir = path.parent().unwrap_or(Path::new("contracts"));
522 Err(format!(
523 "--strict-test-binding cannot run over a single contract file ({}): \
524 the gate resolves cited test names against a source tree rooted at \
525 that file's parent directory, which holds contracts and no source, \
526 so every reference would be reported missing -- including those that \
527 do resolve. Run the directory form instead: \
528 `pv lint {} --strict-test-binding`.",
529 path.display(),
530 dir.display(),
531 )
532 .into())
533}
534
535fn refuse_missing_corpus(contract_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
536 if crate::contract_walk::has_contract_files(contract_dir) {
537 return Ok(());
538 }
539 Err(crate::contract_walk::ZeroContracts {
540 path: contract_dir.to_path_buf(),
541 filter: None,
542 }
543 .into())
544}
545
546fn run_diff_check(
547 contract_dir: &Path,
548 base: &str,
549) -> Option<Result<(), Box<dyn std::error::Error>>> {
550 match provable_contracts::lint::diff::changed_contracts(contract_dir, base) {
551 Ok(changed) if changed.is_empty() => {
552 if !crate::contract_walk::has_contract_files(contract_dir) {
556 return Some(Err(crate::contract_walk::ZeroContracts {
557 path: contract_dir.to_path_buf(),
558 filter: None,
559 }
560 .into()));
561 }
562 println!("No contracts changed since {base}. Nothing to lint.");
563 Some(Ok(()))
564 }
565 Ok(changed) => {
566 println!(
567 "Diff-aware: {} contracts changed since {base}",
568 changed.len()
569 );
570 for stem in &changed {
571 println!(" {stem}");
572 }
573 println!();
574 None
575 }
576 Err(e) => {
577 eprintln!("Warning: diff-aware mode failed ({e}), linting all contracts");
578 None
579 }
580 }
581}
582
583fn print_cache_stats(report: &LintReport) {
584 eprintln!(
585 "Cache: {} total, {} hits, {} misses ({:.0}% hit rate)",
586 report.cache_stats.total,
587 report.cache_stats.hits,
588 report.cache_stats.misses,
589 report.cache_stats.hit_rate() * 100.0,
590 );
591}
592
593fn record_trend(contract_dir: &Path, report: &LintReport) {
594 let trend_root = trend::trend_dir(contract_dir);
595 let contracts_count = count_contracts(report);
596 match trend::record_snapshot(&trend_root, report, contracts_count) {
597 Ok(path) => eprintln!("Trend snapshot saved: {}", path.display()),
598 Err(e) => eprintln!("Warning: failed to save trend snapshot: {e}"),
599 }
600 let snapshots = trend::load_snapshots(&trend_root);
601 if let Some(drop) = trend::detect_drift(&snapshots, 0.05) {
602 eprintln!("Warning: quality drift detected (score dropped {drop:.3})");
603 }
604}
605
606fn print_report(format: &str, report: &LintReport) -> Result<(), Box<dyn std::error::Error>> {
607 match format {
608 "json" => lint_render::print_json(report)?,
609 "sarif" => lint_render::print_sarif(report),
610 "github" => lint_render::print_github(report),
611 "html" => println!("{}", lint_html::render_html(report)),
612 _ => lint_render::print_text(report),
613 }
614 Ok(())
615}
616
617fn count_contracts(report: &LintReport) -> usize {
618 for gate in &report.gates {
619 match &gate.detail {
620 GateDetail::Validate { contracts, .. }
621 | GateDetail::Audit { contracts, .. }
622 | GateDetail::Score { contracts, .. } => return *contracts,
623 GateDetail::Verify { .. }
624 | GateDetail::Enforce { .. }
625 | GateDetail::ReverseCoverage { .. }
626 | GateDetail::Composition { .. }
627 | GateDetail::Skipped { .. } => {}
628 }
629 }
630 0
631}
632
633#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
635fn run_watch(
636 contract_dir: &Path,
637 binding_path: Option<&Path>,
638 min_score: f64,
639 format: Option<&str>,
640 severity: Option<&str>,
641 strict: bool,
642 suppress: Option<&str>,
643 suppress_rule: Option<&str>,
644 suppress_file: Option<&str>,
645 rule_overrides: &[String],
646 config_path: Option<&Path>,
647 no_cache: bool,
648 cache_stats: bool,
649 crate_dir: Option<&Path>,
650 min_level: Option<&str>,
651 strict_test_binding: bool,
652) -> Result<(), Box<dyn std::error::Error>> {
653 loop {
654 let config = build_config(
655 contract_dir,
656 binding_path,
657 min_score,
658 format,
659 severity,
660 strict,
661 suppress,
662 suppress_rule,
663 suppress_file,
664 rule_overrides,
665 config_path,
666 no_cache,
667 cache_stats,
668 crate_dir,
669 min_level,
670 strict_test_binding,
671 );
672
673 let mut report = run_lint(&config);
674
675 refuse_empty_corpus(&report, contract_dir)?;
678 report.arm(&lint_arming::declared(contract_dir)?);
680
681 if cache_stats {
682 print_cache_stats(&report);
683 }
684
685 let effective_format = resolve_format(format, config_path, contract_dir);
686 print_report(&effective_format, &report)?;
687
688 println!("\n--- Watching for changes (Ctrl+C to stop) ---\n");
689 std::thread::sleep(std::time::Duration::from_secs(5));
690 }
691}
692
693fn resolve_format(format: Option<&str>, config_path: Option<&Path>, contract_dir: &Path) -> String {
694 if let Some(f) = format {
696 return f.to_string();
697 }
698 let pv_config = config_path
700 .and_then(|cp| load_config(cp).ok())
701 .or_else(|| find_config(contract_dir).and_then(|p| load_config(&p).ok()))
702 .unwrap_or_default();
703 pv_config.output.format.unwrap_or_else(|| "text".into())
704}
705
706#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
707fn build_config<'a>(
708 contract_dir: &'a Path,
709 binding_path: Option<&'a Path>,
710 min_score: f64,
711 _format: Option<&str>,
712 severity: Option<&str>,
713 strict: bool,
714 suppress: Option<&str>,
715 suppress_rule: Option<&str>,
716 suppress_file: Option<&str>,
717 rule_overrides: &[String],
718 config_path: Option<&Path>,
719 no_cache: bool,
720 cache_stats: bool,
721 crate_dir: Option<&'a Path>,
722 min_level: Option<&str>,
723 strict_test_binding: bool,
724) -> LintConfig<'a> {
725 let pv_config = config_path
726 .and_then(|cp| match load_config(cp) {
727 Ok(c) => {
728 if c.lint.min_score.is_none()
729 && !c.lint.strict
730 && c.lint.severity.is_none()
731 && c.lint.rules.is_empty()
732 && c.output.format.is_none()
733 {
734 eprintln!(
735 "Warning: config {} parsed but no lint settings found",
736 cp.display()
737 );
738 }
739 Some(c)
740 }
741 Err(e) => {
742 eprintln!("Warning: failed to load config {}: {e}", cp.display());
743 None
744 }
745 })
746 .or_else(|| find_config(contract_dir).and_then(|p| load_config(&p).ok()))
747 .unwrap_or_default();
748
749 let effective_min_score = if min_score > 0.0 {
750 min_score
751 } else {
752 pv_config.lint.min_score.unwrap_or(0.0)
753 };
754
755 let severity_filter = severity
756 .or(pv_config.lint.severity.as_deref())
757 .and_then(RuleSeverity::from_str_opt);
758
759 let effective_strict = strict || pv_config.lint.strict;
760
761 let suppressed_findings: Vec<String> = parse_csv(suppress)
762 .into_iter()
763 .chain(pv_config.lint.suppress.findings.iter().cloned())
764 .collect();
765 let suppressed_rules: Vec<String> = parse_csv(suppress_rule)
766 .into_iter()
767 .chain(pv_config.lint.suppress.rules.iter().cloned())
768 .collect();
769 let suppressed_files: Vec<String> = parse_csv(suppress_file)
770 .into_iter()
771 .chain(pv_config.lint.suppress.files.iter().cloned())
772 .collect();
773
774 let mut severity_overrides = std::collections::HashMap::new();
775 for entry in &pv_config.lint.rules {
776 if let Some(sev) = RuleSeverity::from_str_opt(entry.1) {
777 severity_overrides.insert(entry.0.clone(), sev);
778 }
779 }
780 for r in rule_overrides {
781 if let Some((id, sev_str)) = r.split_once('=') {
782 if let Some(sev) = RuleSeverity::from_str_opt(sev_str) {
783 severity_overrides.insert(id.to_string(), sev);
784 }
785 }
786 }
787
788 LintConfig {
789 contract_dir,
790 binding_path,
791 min_score: effective_min_score,
792 severity_filter,
793 severity_overrides,
794 suppressed_findings,
795 suppressed_rules,
796 suppressed_files,
797 strict: effective_strict,
798 no_cache,
799 cache_stats,
800 crate_dir,
801 min_level: min_level.and_then(parse_enforcement_level),
802 strict_test_binding,
803 }
804}
805
806fn parse_enforcement_level(s: &str) -> Option<provable_contracts::schema::EnforcementLevel> {
807 use provable_contracts::schema::EnforcementLevel;
808 match s.to_lowercase().as_str() {
809 "basic" => Some(EnforcementLevel::Basic),
810 "standard" => Some(EnforcementLevel::Standard),
811 "strict" => Some(EnforcementLevel::Strict),
812 "proven" => Some(EnforcementLevel::Proven),
813 _ => None,
814 }
815}
816
817fn parse_csv(s: Option<&str>) -> Vec<String> {
818 s.map(|v| {
819 v.split(',')
820 .map(|s| s.trim().to_string())
821 .filter(|s| !s.is_empty())
822 .collect()
823 })
824 .unwrap_or_default()
825}
826
827fn collect_yaml_files_lint(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
829 let Ok(entries) = std::fs::read_dir(dir) else {
830 return;
831 };
832 for entry in entries.flatten() {
833 let path = entry.path();
834 if path.is_dir() {
835 let dirname = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
836 if dirname == "kaizen" || dirname == "legacy" || dirname == "pipelines" {
837 continue;
838 }
839 collect_yaml_files_lint(&path, out);
840 } else if path.extension().and_then(|e| e.to_str()) == Some("yaml")
841 && !matches!(
842 path.file_name().and_then(|n| n.to_str()),
843 Some("binding.yaml" | "binding.yml")
844 )
845 {
846 out.push(path);
847 }
848 }
849}