1- use std:: collections:: { BTreeMap , HashSet } ;
1+ use std:: {
2+ collections:: { BTreeMap , HashSet } ,
3+ sync:: Arc ,
4+ } ;
25
6+ use camino:: Utf8Path ;
37use itertools:: Itertools ;
48use miette:: { NamedSource , SourceSpan } ;
5- use oxc_resolver:: { ResolveError , Resolver } ;
9+ use oxc_resolver:: { ResolveError , Resolver , TsConfig } ;
10+ use swc_common:: { comments:: SingleThreadedComments , SourceFile , Span } ;
611use turbo_trace:: ImportType ;
7- use turbopath:: { AbsoluteSystemPath , PathRelation , RelativeUnixPath } ;
12+ use turbopath:: { AbsoluteSystemPath , AnchoredSystemPathBuf , PathRelation , RelativeUnixPath } ;
813use turborepo_repository:: {
9- package_graph:: { PackageName , PackageNode } ,
14+ package_graph:: { PackageInfo , PackageName , PackageNode } ,
1015 package_json:: PackageJson ,
1116} ;
1217
1318use crate :: {
14- boundaries:: { BoundariesDiagnostic , Error } ,
19+ boundaries:: { tsconfig :: TsConfigLoader , BoundariesDiagnostic , BoundariesResult , Error } ,
1520 run:: Run ,
1621} ;
1722
1823impl Run {
24+ /// Checks if the given import can be resolved as a tsconfig path alias,
25+ /// e.g. `@/types/foo` -> `./src/foo`, and if so, checks the resolved paths.
26+ ///
27+ /// Returns true if the import was resolved as a tsconfig path alias.
28+ #[ allow( clippy:: too_many_arguments) ]
29+ fn check_import_as_tsconfig_path_alias (
30+ & self ,
31+ tsconfig_loader : & mut TsConfigLoader ,
32+ package_name : & PackageName ,
33+ package_root : & AbsoluteSystemPath ,
34+ span : SourceSpan ,
35+ file_path : & AbsoluteSystemPath ,
36+ file_content : & str ,
37+ import : & str ,
38+ result : & mut BoundariesResult ,
39+ ) -> Result < bool , Error > {
40+ let dir = file_path. parent ( ) . expect ( "file_path must have a parent" ) ;
41+ let Some ( tsconfig) = tsconfig_loader. load ( dir, result) else {
42+ return Ok ( false ) ;
43+ } ;
44+
45+ let resolved_paths = tsconfig. resolve ( dir. as_std_path ( ) , import) ;
46+ for path in & resolved_paths {
47+ let Some ( utf8_path) = Utf8Path :: from_path ( path) else {
48+ result. diagnostics . push ( BoundariesDiagnostic :: InvalidPath {
49+ path : path. to_string_lossy ( ) . to_string ( ) ,
50+ } ) ;
51+ continue ;
52+ } ;
53+ let resolved_import_path = AbsoluteSystemPath :: new ( utf8_path) ?;
54+ result. diagnostics . extend ( self . check_file_import (
55+ file_path,
56+ package_root,
57+ package_name,
58+ import,
59+ resolved_import_path,
60+ span,
61+ file_content,
62+ ) ?) ;
63+ }
64+
65+ Ok ( !resolved_paths. is_empty ( ) )
66+ }
67+
68+ #[ allow( clippy:: too_many_arguments) ]
69+ pub ( crate ) fn check_import (
70+ & self ,
71+ comments : & SingleThreadedComments ,
72+ tsconfig_loader : & mut TsConfigLoader ,
73+ result : & mut BoundariesResult ,
74+ source_file : & Arc < SourceFile > ,
75+ package_name : & PackageName ,
76+ package_root : & AbsoluteSystemPath ,
77+ import : & str ,
78+ import_type : & ImportType ,
79+ span : & Span ,
80+ file_path : & AbsoluteSystemPath ,
81+ file_content : & str ,
82+ package_info : & PackageInfo ,
83+ internal_dependencies : & HashSet < & PackageNode > ,
84+ unresolved_external_dependencies : Option < & BTreeMap < String , String > > ,
85+ resolver : & Resolver ,
86+ ) -> Result < ( ) , Error > {
87+ // If the import is prefixed with `@boundaries-ignore`, we ignore it, but print
88+ // a warning
89+ match Self :: get_ignored_comment ( comments, * span) {
90+ Some ( reason) if reason. is_empty ( ) => {
91+ result. warnings . push (
92+ "@boundaries-ignore requires a reason, e.g. `// @boundaries-ignore implicit \
93+ dependency`"
94+ . to_string ( ) ,
95+ ) ;
96+ }
97+ Some ( _) => {
98+ // Try to get the line number for warning
99+ let line = result. source_map . lookup_line ( span. lo ( ) ) . map ( |l| l. line ) ;
100+ if let Ok ( line) = line {
101+ result
102+ . warnings
103+ . push ( format ! ( "ignoring import on line {} in {}" , line, file_path) ) ;
104+ } else {
105+ result
106+ . warnings
107+ . push ( format ! ( "ignoring import in {}" , file_path) ) ;
108+ }
109+
110+ return Ok ( ( ) ) ;
111+ }
112+ None => { }
113+ }
114+
115+ let ( start, end) = result. source_map . span_to_char_offset ( source_file, * span) ;
116+ let start = start as usize ;
117+ let end = end as usize ;
118+
119+ let span = SourceSpan :: new ( start. into ( ) , end - start) ;
120+
121+ if self . check_import_as_tsconfig_path_alias (
122+ tsconfig_loader,
123+ package_name,
124+ package_root,
125+ span,
126+ file_path,
127+ file_content,
128+ import,
129+ result,
130+ ) ? {
131+ return Ok ( ( ) ) ;
132+ }
133+
134+ // We have a file import
135+ let check_result = if import. starts_with ( "." ) {
136+ let import_path = RelativeUnixPath :: new ( import) ?;
137+ let dir_path = file_path
138+ . parent ( )
139+ . ok_or_else ( || Error :: NoParentDir ( file_path. to_owned ( ) ) ) ?;
140+ let resolved_import_path = dir_path. join_unix_path ( import_path) . clean ( ) ?;
141+ self . check_file_import (
142+ file_path,
143+ package_root,
144+ package_name,
145+ import,
146+ & resolved_import_path,
147+ span,
148+ file_content,
149+ ) ?
150+ } else if Self :: is_potential_package_name ( import) {
151+ self . check_package_import (
152+ import,
153+ * import_type,
154+ span,
155+ file_path,
156+ file_content,
157+ & package_info. package_json ,
158+ internal_dependencies,
159+ unresolved_external_dependencies,
160+ resolver,
161+ )
162+ } else {
163+ None
164+ } ;
165+
166+ result. diagnostics . extend ( check_result) ;
167+
168+ Ok ( ( ) )
169+ }
170+
171+ #[ allow( clippy:: too_many_arguments) ]
19172 pub ( crate ) fn check_file_import (
20173 & self ,
21174 file_path : & AbsoluteSystemPath ,
22175 package_path : & AbsoluteSystemPath ,
176+ package_name : & PackageName ,
23177 import : & str ,
178+ resolved_import_path : & AbsoluteSystemPath ,
24179 source_span : SourceSpan ,
25180 file_content : & str ,
26181 ) -> Result < Option < BoundariesDiagnostic > , Error > {
27- let import_path = RelativeUnixPath :: new ( import) ?;
28- let dir_path = file_path
29- . parent ( )
30- . ok_or_else ( || Error :: NoParentDir ( file_path. to_owned ( ) ) ) ?;
31- let resolved_import_path = dir_path. join_unix_path ( import_path) . clean ( ) ?;
32182 // We have to check for this case because `relation_to_path` returns `Parent` if
33183 // the paths are equal and there's nothing wrong with importing the
34184 // package you're in.
@@ -38,11 +188,17 @@ impl Run {
38188 // We use `relation_to_path` and not `contains` because `contains`
39189 // panics on invalid paths with too many `..` components
40190 if !matches ! (
41- package_path. relation_to_path( & resolved_import_path) ,
191+ package_path. relation_to_path( resolved_import_path) ,
42192 PathRelation :: Parent
43193 ) {
194+ let resolved_import_path =
195+ AnchoredSystemPathBuf :: relative_path_between ( package_path, resolved_import_path)
196+ . to_string ( ) ;
197+
44198 Ok ( Some ( BoundariesDiagnostic :: ImportLeavesPackage {
45199 import : import. to_string ( ) ,
200+ resolved_import_path,
201+ package_name : package_name. to_owned ( ) ,
46202 span : source_span,
47203 text : NamedSource :: new ( file_path. as_str ( ) , file_content. to_string ( ) ) ,
48204 } ) )
0 commit comments