tree_sitter_stack_graphs_typescript/
tsconfig.rs1use glob::Pattern;
9use std::collections::HashMap;
10use std::path::Component;
11use std::path::Path;
12use std::path::PathBuf;
13
14use stack_graphs::arena::Handle;
15use stack_graphs::graph::File;
16use stack_graphs::graph::StackGraph;
17use tree_sitter_stack_graphs::BuildError;
18use tree_sitter_stack_graphs::FileAnalyzer;
19
20use crate::util::*;
21
22pub struct TsConfigAnalyzer {}
23
24impl FileAnalyzer for TsConfigAnalyzer {
25 fn build_stack_graph_into<'a>(
26 &self,
27 graph: &mut StackGraph,
28 file: Handle<File>,
29 path: &Path,
30 source: &str,
31 all_paths: &mut dyn Iterator<Item = &'a Path>,
32 globals: &HashMap<String, String>,
33 _cancellation_flag: &dyn tree_sitter_stack_graphs::CancellationFlag,
34 ) -> Result<(), tree_sitter_stack_graphs::BuildError> {
35 let proj_name = globals.get(crate::PROJECT_NAME_VAR).map(String::as_str);
37
38 let tsc = TsConfig::parse_str(path, source).map_err(|_| BuildError::ParseError)?;
40
41 let root = StackGraph::root_node();
43
44 let proj_scope = if let Some(proj_name) = proj_name {
46 let proj_scope_id = graph.new_node_id(file);
47 let proj_scope = graph
48 .add_scope_node(proj_scope_id, false)
49 .expect("no previous node for new id");
50 add_debug_name(graph, proj_scope, "tsconfig.proj_scope");
51
52 let proj_def = add_ns_pop(graph, file, root, PROJ_NS, proj_name, "tsconfig.proj_def");
54 add_edge(graph, proj_def, proj_scope, 0);
55
56 let proj_ref = add_ns_push(graph, file, root, PROJ_NS, proj_name, "tsconfig.proj_ref");
58 add_edge(graph, proj_scope, proj_ref, 0);
59
60 proj_scope
61 } else {
62 root
63 };
64
65 let pkg_def = add_pop(graph, file, proj_scope, PKG_M_NS, "tsconfig.pkg_def");
67 let root_dir_ref = add_module_pushes(
68 graph,
69 file,
70 M_NS,
71 &tsc.root_dir(all_paths),
72 proj_scope,
73 "tsconfig.root_dir.ref",
74 );
75 add_edge(graph, pkg_def, root_dir_ref, 0);
76
77 for (idx, root_dir) in tsc.root_dirs().iter().enumerate() {
79 let root_dir_def = add_pop(
80 graph,
81 file,
82 proj_scope,
83 REL_M_NS,
84 &format!("tsconfig.root_dirs[{}].def", idx),
85 );
86 let root_dir_ref = add_module_pushes(
87 graph,
88 file,
89 M_NS,
90 root_dir,
91 proj_scope,
92 &format!("tsconfig.root_dirs[{}].ref", idx),
93 );
94 add_edge(graph, root_dir_def, root_dir_ref, 0);
95 }
96
97 let base_url = tsc.base_url();
99 let base_url_def = add_pop(
100 graph,
101 file,
102 proj_scope,
103 NON_REL_M_NS,
104 "tsconfig.base_url.def",
105 );
106 let base_url_ref = add_module_pushes(
107 graph,
108 file,
109 M_NS,
110 &base_url,
111 proj_scope,
112 "tsconfig.base_url.ref",
113 );
114 add_edge(graph, base_url_def, base_url_ref, 0);
115
116 for (from_idx, (from, tos)) in tsc.paths().iter().enumerate() {
118 let is_prefix = from.file_name().map_or(false, |n| n == "*");
119 let from = if is_prefix {
120 match from.parent() {
121 Some(from) => from,
122 None => continue,
123 }
124 } else {
125 &from
126 };
127 let from_def = add_module_pops(
128 graph,
129 file,
130 NON_REL_M_NS,
131 from,
132 proj_scope,
133 &format!("tsconfig.paths[{}].from_def", from_idx),
134 );
135 for (to_idx, to) in tos.iter().enumerate() {
136 if is_prefix && !to.file_name().map_or(false, |n| n == "*") {
137 continue;
138 }
139 let to = if is_prefix {
140 match to.parent() {
141 Some(to) => to,
142 None => continue,
143 }
144 } else {
145 &to
146 };
147 let to_ref = add_module_pushes(
148 graph,
149 file,
150 M_NS,
151 to,
152 proj_scope,
153 &format!("tsconfig.paths[{}][{}].to_ref", from_idx, to_idx),
154 );
155 add_edge(graph, from_def, to_ref, 0);
156 }
157 }
158
159 Ok(())
160 }
161}
162
163const TS_EXT: &str = "ts";
166const TSX_EXT: &str = "tsx";
167const JS_EXT: &str = "js";
168const JSX_EXT: &str = "jsx";
169const D_TS_EXT: &str = "d.ts";
170
171struct TsConfig {
172 project_dir: PathBuf,
173 tsc: tsconfig::TsConfig,
174}
175
176impl TsConfig {
177 fn parse_str(path: &Path, source: &str) -> Result<Self, BuildError> {
178 let project_dir = path.parent().ok_or(BuildError::ParseError)?.to_path_buf();
179 let tsc = tsconfig::TsConfig::parse_str(source).map_err(|_| BuildError::ParseError)?;
180 Ok(Self { project_dir, tsc })
181 }
182}
183
184impl TsConfig {
185 pub(self) fn allow_js(&self) -> bool {
189 self.tsc
190 .compiler_options
191 .as_ref()
192 .map_or(false, |co| co.allow_js.unwrap_or(false))
193 }
194
195 pub(self) fn base_url(&self) -> PathBuf {
199 self.tsc
200 .compiler_options
201 .as_ref()
202 .map_or(PathBuf::new(), |co| {
203 co.base_url
204 .as_ref()
205 .and_then(|p| {
206 NormalizedRelativePath::from_str(p)
207 .filter(|p| !p.escapes())
208 .map(|p| p.into_path_buf())
209 })
210 .unwrap_or(PathBuf::default())
211 })
212 }
213
214 pub(self) fn composite(&self) -> bool {
218 self.tsc
219 .compiler_options
220 .as_ref()
221 .map_or(false, |co| co.composite.unwrap_or(false))
222 }
223
224 pub(self) fn exclude(&self) -> Vec<Pattern> {
228 self.tsc.exclude.as_ref().map_or(vec![], |patterns| {
229 patterns
230 .iter()
231 .flat_map(|p| self.expand_patterns(p))
232 .collect()
233 })
234 }
235
236 pub(self) fn files(&self) -> Vec<PathBuf> {
240 self.tsc
241 .files
242 .as_ref()
243 .map_or(vec![], |e| e.iter().map(PathBuf::from).collect())
244 }
245
246 fn has_files(&self) -> bool {
248 self.tsc.files.is_some()
249 }
250
251 pub(self) fn include(&self) -> Vec<Pattern> {
255 if let Some(patterns) = &self.tsc.include {
256 patterns
258 .iter()
259 .flat_map(|p| self.expand_patterns(p))
260 .collect()
261 } else if self.has_files() {
262 vec![]
264 } else {
265 self.expand_patterns("**/*")
267 }
268 }
269
270 fn expand_patterns(&self, pattern: &str) -> Vec<Pattern> {
272 let mut p = PathBuf::from(pattern);
273
274 if p.extension().is_some() {
276 return Pattern::new(&pattern).map_or(vec![], |p| vec![p]);
277 }
278
279 if p.file_name().map_or(true, |n| n == "**") {
281 p.push("*");
282 }
283
284 let mut es = vec![TS_EXT, TSX_EXT, D_TS_EXT];
286 if self.allow_js() {
287 es.extend(&[JS_EXT, JSX_EXT]);
288 }
289
290 es.into_iter()
292 .filter_map(|e| {
293 p.with_extension(e)
294 .to_str()
295 .and_then(|p| Pattern::new(p).ok())
296 })
297 .collect()
298 }
299
300 pub(self) fn paths(&self) -> HashMap<PathBuf, Vec<PathBuf>> {
302 self.tsc
303 .compiler_options
304 .as_ref()
305 .map_or(HashMap::default(), |co| {
306 co.paths.as_ref().map_or(HashMap::default(), |ps| {
307 let mut m = HashMap::new();
308 for (key, values) in ps {
309 let from = match NormalizedRelativePath::from_str(key) {
310 Some(from) => from,
311 None => continue,
312 };
313 if from.escapes() {
314 continue;
315 }
316 let is_prefix = from.as_path().file_name().map_or(false, |n| n == "*");
317 let base_url = self.base_url();
318 let tos = values
319 .iter()
320 .filter_map(|v| {
321 let to = match NormalizedRelativePath::from_path(
322 &base_url.as_path().join(v),
323 ) {
324 Some(to) => to,
325 None => return None,
326 };
327 if from.escapes() {
328 return None;
329 }
330 if is_prefix
331 && !from.as_path().file_name().map_or(false, |n| n == "*")
332 {
333 return None;
334 }
335 Some(to.into())
336 })
337 .collect();
338 m.insert(from.into(), tos);
339 }
340 m
341 })
342 })
343 }
344
345 pub(self) fn root_dir<'a, PI>(&self, source_paths: PI) -> PathBuf
358 where
359 PI: IntoIterator<Item = &'a Path>,
360 {
361 if let Some(root_dir) = self
362 .tsc
363 .compiler_options
364 .as_ref()
365 .and_then(|co| {
366 co.root_dir
367 .as_ref()
368 .map(|p| NormalizedRelativePath::from_str(&p))
369 })
370 .flatten()
371 .filter(|p| !p.escapes())
372 {
373 return root_dir.into();
374 }
375
376 if self.composite() {
377 return PathBuf::default();
378 }
379
380 let mut root_dir: Option<PathBuf> = None;
381 for input_path in self.input_files(source_paths) {
382 if input_path
383 .extension()
384 .map(|ext| ext == D_TS_EXT)
385 .unwrap_or(false)
386 {
387 continue;
388 }
389
390 let input_dir = match input_path.parent() {
391 Some(input_dir) => input_dir,
392 None => continue,
393 };
394
395 root_dir = Some(if let Some(root_dir) = root_dir {
396 longest_common_prefix(&root_dir, input_dir).unwrap_or(root_dir)
397 } else {
398 input_dir.to_path_buf()
399 });
400 }
401
402 root_dir.unwrap_or(PathBuf::default())
403 }
404
405 pub(self) fn root_dirs(&self) -> Vec<PathBuf> {
409 self.tsc.compiler_options.as_ref().map_or(vec![], |co| {
410 co.root_dirs.as_ref().map_or(vec![], |rs| {
411 rs.iter()
412 .flat_map(|r| NormalizedRelativePath::from_str(r))
413 .filter(|r| !r.escapes())
414 .map(|r| r.into_path_buf())
415 .collect()
416 })
417 })
418 }
419
420 fn input_files<'a, PI>(&self, source_paths: PI) -> Vec<PathBuf>
422 where
423 PI: IntoIterator<Item = &'a Path>,
424 {
425 let files = self.files();
426 let include = self.include();
427 let exclude = self.exclude();
428
429 source_paths
430 .into_iter()
431 .filter_map(|p| {
432 let p = match p.strip_prefix(&self.project_dir) {
433 Ok(p) => p,
434 Err(_) => return None,
435 };
436
437 let p = match NormalizedRelativePath::from_path(p) {
439 Some(p) => p.into_path_buf(),
440 None => return None,
441 };
442
443 for file in &files {
445 if &p == file {
446 return Some(p);
447 }
448 }
449
450 if !include.iter().any(|i| i.matches_path(&p)) {
452 return None;
453 }
454
455 if exclude.iter().any(|e| e.matches_path(&p)) {
457 return None;
458 }
459
460 Some(p)
462 })
463 .collect()
464 }
465}
466
467fn longest_common_prefix(left: &Path, right: &Path) -> Option<PathBuf> {
471 let mut prefix = PathBuf::new();
472 let mut left_it = left.components();
473 let mut right_it = right.components();
474 loop {
475 match (left_it.next(), right_it.next()) {
476 (Some(sc @ Component::Prefix(sp)), Some(Component::Prefix(op))) if sp == op => {
478 prefix.push(sc);
479 }
480 (Some(Component::Prefix(_)), _) | (_, Some(Component::Prefix(_))) => {
481 return None;
482 }
483 (Some(sc @ Component::RootDir), Some(Component::RootDir)) => {
485 prefix.push(sc);
486 }
487 (Some(Component::RootDir), _) | (_, Some(Component::RootDir)) => {
488 return None;
489 }
490 (Some(sc), Some(oc)) if sc == oc => {
492 prefix.push(sc);
493 }
494 (_, _) => break,
496 }
497 }
498 Some(prefix)
499}
500
501pub(crate) struct NormalizedRelativePath(PathBuf);
502
503impl NormalizedRelativePath {
504 pub(crate) fn from_str(path: &str) -> Option<Self> {
505 Self::from_path(Path::new(path))
506 }
507
508 pub(crate) fn from_path(path: &Path) -> Option<Self> {
510 let mut np = PathBuf::new();
511 let mut normal_components = 0usize;
512 for c in path.components() {
513 match c {
514 Component::Prefix(_) => {
515 return None;
516 }
517 Component::RootDir => {
518 return None;
519 }
520 Component::CurDir => {}
521 Component::ParentDir => {
522 if normal_components > 0 {
523 normal_components -= 1;
525 np.pop();
526 } else {
527 np.push(c);
529 }
530 }
531 Component::Normal(_) => {
532 normal_components += 1;
533 np.push(c);
534 }
535 }
536 }
537 Some(Self(np))
538 }
539
540 pub(crate) fn escapes(&self) -> bool {
542 self.0
543 .components()
544 .next()
545 .map_or(false, |c| c == Component::ParentDir)
546 }
547
548 pub(crate) fn as_path(&self) -> &Path {
549 &self.0
550 }
551
552 pub(crate) fn into_path_buf(self) -> PathBuf {
553 self.0
554 }
555}
556
557impl AsRef<Path> for NormalizedRelativePath {
558 fn as_ref(&self) -> &Path {
559 &self.0
560 }
561}
562
563impl Into<PathBuf> for NormalizedRelativePath {
564 fn into(self) -> PathBuf {
565 self.0
566 }
567}