1
- use std:: collections:: { BTreeMap , HashSet } ;
1
+ use std:: {
2
+ collections:: { BTreeMap , HashSet } ,
3
+ sync:: Arc ,
4
+ } ;
2
5
6
+ use camino:: Utf8Path ;
3
7
use itertools:: Itertools ;
4
8
use miette:: { NamedSource , SourceSpan } ;
5
- use oxc_resolver:: { ResolveError , Resolver } ;
9
+ use oxc_resolver:: { ResolveError , Resolver , TsConfig } ;
10
+ use swc_common:: { comments:: SingleThreadedComments , SourceFile , Span } ;
6
11
use turbo_trace:: ImportType ;
7
- use turbopath:: { AbsoluteSystemPath , PathRelation , RelativeUnixPath } ;
12
+ use turbopath:: { AbsoluteSystemPath , AnchoredSystemPathBuf , PathRelation , RelativeUnixPath } ;
8
13
use turborepo_repository:: {
9
- package_graph:: { PackageName , PackageNode } ,
14
+ package_graph:: { PackageInfo , PackageName , PackageNode } ,
10
15
package_json:: PackageJson ,
11
16
} ;
12
17
13
18
use crate :: {
14
- boundaries:: { BoundariesDiagnostic , Error } ,
19
+ boundaries:: { tsconfig :: TsConfigLoader , BoundariesDiagnostic , BoundariesResult , Error } ,
15
20
run:: Run ,
16
21
} ;
17
22
18
23
impl 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) ]
19
172
pub ( crate ) fn check_file_import (
20
173
& self ,
21
174
file_path : & AbsoluteSystemPath ,
22
175
package_path : & AbsoluteSystemPath ,
176
+ package_name : & PackageName ,
23
177
import : & str ,
178
+ resolved_import_path : & AbsoluteSystemPath ,
24
179
source_span : SourceSpan ,
25
180
file_content : & str ,
26
181
) -> 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 ( ) ?;
32
182
// We have to check for this case because `relation_to_path` returns `Parent` if
33
183
// the paths are equal and there's nothing wrong with importing the
34
184
// package you're in.
@@ -38,11 +188,17 @@ impl Run {
38
188
// We use `relation_to_path` and not `contains` because `contains`
39
189
// panics on invalid paths with too many `..` components
40
190
if !matches ! (
41
- package_path. relation_to_path( & resolved_import_path) ,
191
+ package_path. relation_to_path( resolved_import_path) ,
42
192
PathRelation :: Parent
43
193
) {
194
+ let resolved_import_path =
195
+ AnchoredSystemPathBuf :: relative_path_between ( package_path, resolved_import_path)
196
+ . to_string ( ) ;
197
+
44
198
Ok ( Some ( BoundariesDiagnostic :: ImportLeavesPackage {
45
199
import : import. to_string ( ) ,
200
+ resolved_import_path,
201
+ package_name : package_name. to_owned ( ) ,
46
202
span : source_span,
47
203
text : NamedSource :: new ( file_path. as_str ( ) , file_content. to_string ( ) ) ,
48
204
} ) )
0 commit comments