1use solar_ast::{Item, SourceUnit, visit::Visit as VisitAst};
2use solar_interface::SourceMap;
3use solar_parse::ast::Span;
4use solar_sema::hir::{self, Visit as VisitHir};
5use std::{collections::HashMap, fmt, marker::PhantomData, ops::ControlFlow};
6
7#[derive(Clone, Debug)]
9pub enum InlineConfigItem {
10 DisableNextItem(Vec<String>),
12 DisableLine(Vec<String>),
14 DisableNextLine(Vec<String>),
16 DisableStart(Vec<String>),
18 DisableEnd(Vec<String>),
20}
21
22impl InlineConfigItem {
23 pub fn parse(s: &str, lint_ids: &[&str]) -> Result<Self, InvalidInlineConfigItem> {
25 let (disable, relevant) = s.split_once('(').unwrap_or((s, ""));
26 let lints = if relevant.is_empty() || relevant == "all)" {
27 vec!["all".to_string()]
28 } else {
29 match relevant.split_once(')') {
30 Some((lint, _)) => lint.split(",").map(|s| s.trim().to_string()).collect(),
31 None => return Err(InvalidInlineConfigItem::Syntax(s.into())),
32 }
33 };
34
35 let mut invalid_ids = Vec::new();
37 'ids: for id in &lints {
38 if id == "all" {
39 continue;
40 }
41 for lint in lint_ids {
42 if *lint == id {
43 continue 'ids;
44 }
45 }
46 invalid_ids.push(id.to_owned());
47 }
48
49 if !invalid_ids.is_empty() {
50 return Err(InvalidInlineConfigItem::LintIds(invalid_ids));
51 }
52
53 let res = match disable {
54 "disable-next-item" => Self::DisableNextItem(lints),
55 "disable-line" => Self::DisableLine(lints),
56 "disable-next-line" => Self::DisableNextLine(lints),
57 "disable-start" => Self::DisableStart(lints),
58 "disable-end" => Self::DisableEnd(lints),
59 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
60 };
61
62 Ok(res)
63 }
64}
65
66#[derive(Debug)]
67pub enum InvalidInlineConfigItem {
68 Syntax(String),
69 LintIds(Vec<String>),
70}
71
72impl fmt::Display for InvalidInlineConfigItem {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::Syntax(s) => write!(f, "invalid inline config item: {s}"),
76 Self::LintIds(ids) => {
77 write!(f, "unknown lint id: '{}'", ids.join("', '"))
78 }
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy)]
87struct DisabledRange {
88 start: usize,
89 end: usize,
90 loose: bool,
91}
92
93impl DisabledRange {
94 fn includes(&self, range: std::ops::Range<usize>) -> bool {
95 range.start >= self.start && (if self.loose { range.start } else { range.end } <= self.end)
96 }
97}
98
99#[derive(Debug, Default)]
101pub struct InlineConfig {
102 disabled_ranges: HashMap<String, Vec<DisabledRange>>,
103}
104
105impl InlineConfig {
106 pub fn from_ast<'ast>(
113 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
114 ast: &'ast SourceUnit<'ast>,
115 source_map: &SourceMap,
116 ) -> Self {
117 Self::build(items, source_map, |offset| NextItemFinderAst::new(offset).find(ast))
118 }
119
120 pub fn from_hir<'hir>(
127 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
128 hir: &'hir hir::Hir<'hir>,
129 source_id: hir::SourceId,
130 source_map: &SourceMap,
131 ) -> Self {
132 Self::build(items, source_map, |offset| NextItemFinderHir::new(offset, hir).find(source_id))
133 }
134
135 fn build(
136 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
137 source_map: &SourceMap,
138 mut find_next_item: impl FnMut(usize) -> Option<Span>,
139 ) -> Self {
140 let mut disabled_ranges: HashMap<String, Vec<DisabledRange>> = HashMap::new();
141 let mut disabled_blocks: HashMap<String, (usize, usize, usize)> = HashMap::new();
142
143 let mut prev_sp = Span::DUMMY;
144 for (sp, item) in items {
145 if cfg!(debug_assertions) {
146 assert!(sp >= prev_sp, "InlineConfig::new: unsorted items: {sp:?} < {prev_sp:?}");
147 prev_sp = sp;
148 }
149
150 let Ok((file, comment_range)) = source_map.span_to_source(sp) else { continue };
151 let src = file.src.as_str();
152 match item {
153 InlineConfigItem::DisableNextItem(lints) => {
154 if let Some(next_item) = find_next_item(sp.hi().to_usize()) {
155 for lint in lints {
156 disabled_ranges.entry(lint).or_default().push(DisabledRange {
157 start: next_item.lo().to_usize(),
158 end: next_item.hi().to_usize(),
159 loose: false,
160 });
161 }
162 }
163 }
164 InlineConfigItem::DisableLine(lints) => {
165 let start = src[..comment_range.start].rfind('\n').map_or(0, |i| i);
166 let end = src[comment_range.end..]
167 .find('\n')
168 .map_or(src.len(), |i| comment_range.end + i);
169
170 for lint in lints {
171 disabled_ranges.entry(lint).or_default().push(DisabledRange {
172 start: start + file.start_pos.to_usize(),
173 end: end + file.start_pos.to_usize(),
174 loose: false,
175 })
176 }
177 }
178 InlineConfigItem::DisableNextLine(lints) => {
179 if let Some(offset) = src[comment_range.end..].find('\n') {
180 let start = comment_range.end + offset + 1;
181 if start < src.len() {
182 let end = src[start..].find('\n').map_or(src.len(), |i| start + i);
183 for lint in lints {
184 disabled_ranges.entry(lint).or_default().push(DisabledRange {
185 start: start + file.start_pos.to_usize(),
186 end: end + file.start_pos.to_usize(),
187 loose: false,
188 })
189 }
190 }
191 }
192 }
193 InlineConfigItem::DisableStart(lints) => {
194 for lint in lints {
195 disabled_blocks
196 .entry(lint)
197 .and_modify(|(_, depth, _)| *depth += 1)
198 .or_insert((
199 sp.hi().to_usize(),
200 1,
201 file.start_pos.to_usize() + src.len(),
203 ));
204 }
205 }
206 InlineConfigItem::DisableEnd(lints) => {
207 for lint in lints {
208 if let Some((start, depth, _)) = disabled_blocks.get_mut(&lint) {
209 *depth = depth.saturating_sub(1);
210
211 if *depth == 0 {
212 let start = *start;
213 _ = disabled_blocks.remove(&lint);
214
215 disabled_ranges.entry(lint).or_default().push(DisabledRange {
216 start,
217 end: sp.lo().to_usize(),
218 loose: false,
219 })
220 }
221 }
222 }
223 }
224 }
225 }
226
227 for (lint, (start, _, file_end)) in disabled_blocks {
228 disabled_ranges.entry(lint).or_default().push(DisabledRange {
229 start,
230 end: file_end,
231 loose: false,
232 });
233 }
234
235 Self { disabled_ranges }
236 }
237
238 #[inline]
240 pub fn is_disabled(&self, span: Span, lint: &str) -> bool {
241 if let Some(ranges) = self.disabled_ranges.get(lint) {
242 return ranges.iter().any(|range| range.includes(span.to_range()));
243 }
244
245 if let Some(ranges) = self.disabled_ranges.get("all") {
246 return ranges.iter().any(|range| range.includes(span.to_range()));
247 }
248
249 false
250 }
251}
252
253#[derive(Debug, Default)]
255struct NextItemFinderAst<'ast> {
256 offset: usize,
258 _pd: PhantomData<&'ast ()>,
259}
260
261impl<'ast> NextItemFinderAst<'ast> {
262 fn new(offset: usize) -> Self {
263 Self { offset, _pd: PhantomData }
264 }
265
266 fn find(&mut self, ast: &'ast SourceUnit<'ast>) -> Option<Span> {
268 match self.visit_source_unit(ast) {
269 ControlFlow::Break(span) => Some(span),
270 ControlFlow::Continue(()) => None,
271 }
272 }
273}
274
275impl<'ast> VisitAst<'ast> for NextItemFinderAst<'ast> {
276 type BreakValue = Span;
277
278 fn visit_item(&mut self, item: &'ast Item<'ast>) -> ControlFlow<Self::BreakValue> {
279 if item.span.lo().to_usize() > self.offset {
281 return ControlFlow::Break(item.span);
282 }
283
284 self.walk_item(item)
286 }
287}
288
289#[derive(Debug)]
291struct NextItemFinderHir<'hir> {
292 hir: &'hir hir::Hir<'hir>,
293 offset: usize,
295}
296
297impl<'hir> NextItemFinderHir<'hir> {
298 fn new(offset: usize, hir: &'hir hir::Hir<'hir>) -> Self {
299 Self { offset, hir }
300 }
301
302 fn find(&mut self, id: hir::SourceId) -> Option<Span> {
304 match self.visit_nested_source(id) {
305 ControlFlow::Break(span) => Some(span),
306 ControlFlow::Continue(()) => None,
307 }
308 }
309}
310
311impl<'hir> VisitHir<'hir> for NextItemFinderHir<'hir> {
312 type BreakValue = Span;
313
314 fn hir(&self) -> &'hir hir::Hir<'hir> {
315 self.hir
316 }
317
318 fn visit_item(&mut self, item: hir::Item<'hir, 'hir>) -> ControlFlow<Self::BreakValue> {
319 if item.span().lo().to_usize() > self.offset {
321 return ControlFlow::Break(item.span());
322 }
323
324 if item.span().hi().to_usize() < self.offset {
326 return ControlFlow::Continue(());
327 }
328
329 self.walk_item(item)
331 }
332}