1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use compact_str::CompactString;
use shuck_ast::{Name, Span};
/// Which spelling produced an explicit source directive.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceDirectiveOrigin {
/// `# shuck: source=<path>` — the shuck-native spelling. A resolved
/// target is an explicit user assertion and silences the untracked-source
/// diagnostics at the site.
Shuck,
/// `# shellcheck source=<path>` — the ShellCheck-compatible spelling,
/// which keeps ShellCheck's louder not-specified-as-input semantics.
ShellCheck,
}
/// An explicit source directive annotating a `source` reference.
///
/// The asserted target itself lives in [`SourceRefKind::Directive`] (or
/// [`SourceRefKind::DirectiveDevNull`]); this carries the directive's origin
/// and its lint policy. A plain, un-annotated reference has no
/// `SourceDirectiveInfo` at all, so "no directive" is not conflated with any
/// particular directive spelling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceDirectiveInfo {
/// Which spelling produced the directive.
pub origin: SourceDirectiveOrigin,
/// Whether the directive asks for the target to be linted as an
/// additional input (`lint=true`). Defaults to false: assert the target
/// and import its symbols only.
pub lint: bool,
}
/// Broad diagnostic family for a discovered `source`-style reference.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceRefDiagnosticClass {
/// The path is dynamic enough that static resolution is not reliable.
DynamicPath,
/// The path is statically identifiable but may not be tracked by the build.
UntrackedFile,
}
/// One semantic `source` or `.` reference discovered in the file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceRef {
/// Syntactic shape of the referenced path.
pub kind: SourceRefKind,
/// Span of the full `source` command or relevant operand.
pub span: Span,
/// Span of the path-like portion being resolved.
pub path_span: Span,
/// Resolution status computed for this reference.
pub resolution: SourceRefResolution,
/// Whether the path came from an explicitly provided source directive.
pub explicitly_provided: bool,
/// The explicit directive (if any) that annotated this reference.
pub directive: Option<SourceDirectiveInfo>,
/// Diagnostic family higher layers should use for unresolved cases.
pub diagnostic_class: SourceRefDiagnosticClass,
}
impl SourceRef {
/// Whether a shuck-native `# shuck: source=` directive annotated this
/// reference (as opposed to the ShellCheck-compatible spelling or no
/// directive at all).
pub fn has_shuck_directive(&self) -> bool {
self.directive
.is_some_and(|directive| directive.origin == SourceDirectiveOrigin::Shuck)
}
/// Whether the annotating directive asks for the target to be linted as
/// an additional input (`lint=true`).
pub fn lints_target(&self) -> bool {
self.directive.is_some_and(|directive| directive.lint)
}
}
/// Encoded path form for a source-like reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceRefKind {
/// A fully static literal path.
Literal(CompactString),
/// A path injected by a source directive.
Directive(CompactString),
/// A source directive that intentionally resolves to `/dev/null`.
DirectiveDevNull,
/// A path whose value is too dynamic to resolve statically.
Dynamic,
/// A path built from one variable plus a static tail suffix.
SingleVariableStaticTail {
/// Variable that contributes the dynamic prefix.
variable: Name,
/// Static suffix appended after the variable value.
tail: CompactString,
},
}
/// Whether a source-like reference resolved to a tracked target.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceRefResolution {
/// Resolution was not attempted.
Unchecked,
/// Resolution succeeded.
Resolved,
/// Resolution was attempted but no tracked target was found.
Unresolved,
}
pub(crate) fn default_diagnostic_class(kind: &SourceRefKind) -> SourceRefDiagnosticClass {
match kind {
SourceRefKind::DirectiveDevNull | SourceRefKind::Directive(_) => {
SourceRefDiagnosticClass::UntrackedFile
}
SourceRefKind::Literal(_) => SourceRefDiagnosticClass::UntrackedFile,
SourceRefKind::Dynamic => SourceRefDiagnosticClass::DynamicPath,
SourceRefKind::SingleVariableStaticTail { tail, .. } => {
if tail.starts_with('/') {
SourceRefDiagnosticClass::UntrackedFile
} else {
SourceRefDiagnosticClass::DynamicPath
}
}
}
}