@@ -15,6 +15,8 @@ use schemars::JsonSchema;
15
15
16
16
use crate :: services:: dependency_graph:: ResolvedImports ;
17
17
18
+ const INDEX_BASENAMES : & [ & str ] = & [ "index" , "mod" ] ;
19
+
18
20
declare_lint_rule ! {
19
21
/// Restricts imports of private exports.
20
22
///
@@ -24,21 +26,64 @@ declare_lint_rule! {
24
26
/// this makes it hard to enforce module boundaries, or to prevent importing
25
27
/// things that were only exported for test purposes, for instance.
26
28
///
27
- /// This rule recognizes the JSDoc annotations `@public`, `@package`, and
29
+ /// This rule recognizes the JSDoc tags `@public`, `@package`, and
28
30
/// `@private` so that you are free to set the visibility of exports.
29
- /// Exports without annotation have a default visibility of **public**, but
30
- /// this can be configured.
31
+ /// Exports without tag have a default visibility of **public**, but this
32
+ /// can be configured.
33
+ ///
34
+ /// The `@access` tag is also supported if it's used with one of the values
35
+ /// `public`, `package`, or `private`.
36
+ ///
37
+ /// ## Public visibility
38
+ ///
39
+ /// Public visibility is the default and means there are no restrictions for
40
+ /// importing a given symbol. In other words, without this rule, all
41
+ /// exported symbols are implicitly public.
42
+ ///
43
+ /// ## Package visibility
44
+ ///
45
+ /// Within the context of this rule, _package visibility_ means that a
46
+ /// symbol is visible within the same "package", which means that any module
47
+ /// that resides in the same folder, or one of its subfolders, is allowed to
48
+ /// import the symbol. Modules that only share a common folder higher up in
49
+ /// the hierarchy are not allowed to import the symbol.
50
+ ///
51
+ /// For a visual explanation, see
52
+ /// [this illustration](https://github.com/uhyo/eslint-plugin-import-access?tab=readme-ov-file#what).
53
+ ///
54
+ /// ## Private visibility
55
+ ///
56
+ /// Private visibility means that a symbol may not be imported. This may
57
+ /// sound backwards: Why export a symbol at all if you intend for it to be
58
+ /// private?
59
+ ///
60
+ /// But to understand the usefulness of `@private`, we should consider that
61
+ /// this rule doesn't treat modules and files as one and the same thing.
62
+ /// While files are indeed modules, folders are considered modules too, with
63
+ /// their files and subfolders being submodules. Therefore, symbols exported
64
+ /// as `@private` from an index file, such as `index.js`, can _still_ be
65
+ /// imported from other submodules in that same module.
31
66
///
32
- /// By enabling this rule, all exported symbols, such as types, functions
33
- /// or other things that may be exported, are considered to be "package
34
- /// private". This means that modules that reside in the same directory, as
35
- /// well as submodules of those "sibling" modules, are allowed to import
36
- /// them, while any other modules that are further away in the file system
37
- /// are restricted from importing them. A symbol's visibility may be
38
- /// extended by re-exporting from an index file.
67
+ /// :::note
68
+ /// For the sake of compatibility with conventions used with Deno, modules
69
+ /// named `mod.js`/`mod.ts` are considered index files too.
70
+ /// :::
39
71
///
40
- /// Notes:
72
+ /// Another reason why private visibility may still be useful is that it
73
+ /// allows you to choose specific exceptions. For example, using
74
+ /// [overrides](https://biomejs.dev/reference/configuration/#overrides), you
75
+ /// may want to disable this rule in all files with a `.test.js` extension.
76
+ /// This way, symbols marked private cannot be imported from anywhere except
77
+ /// test files.
41
78
///
79
+ /// ## Known Limitations
80
+ ///
81
+ /// * This rule currently only looks at the JSDoc comments that are attached
82
+ /// to the _`export` statement_ nearest to the symbol's definition. If the
83
+ /// symbol isn't exported in the same statement as in which it is defined,
84
+ /// the visibility as specified in `export` statement is used, not that of
85
+ /// the symbol definition. Re-exports cannot override the visibility from
86
+ /// the original `export`.
42
87
/// * This rule only applies to imports for JavaScript and TypeScript
43
88
/// files. Imports for resources such as images or CSS files are exempted
44
89
/// regardless of the default visibility setting.
@@ -96,7 +141,6 @@ declare_lint_rule! {
96
141
/// // import from the index file of a parent module:
97
142
/// import { subPrivateVariable } from "../index.js";
98
143
/// ```
99
- ///
100
144
pub NoPrivateImports {
101
145
version: "next" ,
102
146
name: "noPrivateImports" ,
@@ -113,7 +157,7 @@ declare_lint_rule! {
113
157
#[ cfg_attr( feature = "schemars" , derive( JsonSchema ) ) ]
114
158
#[ serde( rename_all = "camelCase" , deny_unknown_fields, default ) ]
115
159
pub struct NoPrivateImportsOptions {
116
- /// The default visibility to assume for symbols without annotation .
160
+ /// The default visibility to assume for symbols without visibility tag .
117
161
///
118
162
/// Default: **public**.
119
163
pub default_visibility : Visibility ,
@@ -251,12 +295,17 @@ struct GetRestrictedImportOptions<'a> {
251
295
/// Dependency data of the target module we're importing from.
252
296
target_data : ModuleDependencyData ,
253
297
254
- /// The visibility to assume for symbols without explicit visibility
255
- /// annotation.
298
+ /// The visibility to assume for symbols without explicit visibility tag.
256
299
default_visibility : Visibility ,
257
300
}
258
301
259
302
impl GetRestrictedImportOptions < ' _ > {
303
+ /// Returns whether [Self::target_path] is within the same module as
304
+ /// [Self::self_path].
305
+ fn target_path_is_in_same_module ( & self ) -> bool {
306
+ target_path_is_in_same_module_as_self_path ( self . target_path , self . self_path )
307
+ }
308
+
260
309
/// Returns whether [Self::target_path] is within the same package as
261
310
/// [Self::self_path].
262
311
fn target_path_is_in_same_package ( & self ) -> bool {
@@ -368,7 +417,7 @@ fn get_restricted_import_visibility(
368
417
369
418
let is_restricted = match visibility {
370
419
Visibility :: Public => false ,
371
- Visibility :: Private => true ,
420
+ Visibility :: Private => !options . target_path_is_in_same_module ( ) ,
372
421
Visibility :: Package => !options. target_path_is_in_same_package ( ) ,
373
422
} ;
374
423
@@ -387,13 +436,41 @@ fn parse_visibility(jsdoc_comment: &str) -> Option<Visibility> {
387
436
} )
388
437
}
389
438
439
+ /// Returns whether `target_path` is within the same module as `self_path`.
440
+ #[ inline]
441
+ fn target_path_is_in_same_module_as_self_path (
442
+ target_path : & Utf8Path ,
443
+ self_path : & Utf8Path ,
444
+ ) -> bool {
445
+ if !target_path
446
+ . file_stem ( )
447
+ . is_some_and ( |stem| INDEX_BASENAMES . contains ( & stem) )
448
+ {
449
+ return false ;
450
+ }
451
+
452
+ let Some ( target_parent) = target_path. parent ( ) else {
453
+ // If we cannot navigate further up from the target path, it means the
454
+ // target is in the root, which means everything else is in the same
455
+ // module as it.
456
+ return true ;
457
+ } ;
458
+
459
+ self_path
460
+ . ancestors ( )
461
+ . any ( |ancestor| ancestor == target_parent)
462
+ }
463
+
390
464
/// Returns whether `target_path` is within the same package as `self_path`.
391
465
#[ inline]
392
466
fn target_path_is_in_same_package_as_self_path (
393
467
target_path : & Utf8Path ,
394
468
self_path : & Utf8Path ,
395
469
) -> bool {
396
- let target_path = if target_path. file_stem ( ) . is_some_and ( |stem| stem == "index" ) {
470
+ let target_path = if target_path
471
+ . file_stem ( )
472
+ . is_some_and ( |stem| INDEX_BASENAMES . contains ( & stem) )
473
+ {
397
474
target_path. parent ( ) . unwrap_or ( Utf8Path :: new ( "." ) )
398
475
} else {
399
476
target_path
@@ -415,8 +492,77 @@ fn target_path_is_in_same_package_as_self_path(
415
492
mod tests {
416
493
use super :: * ;
417
494
495
+ #[ test]
496
+ fn test_target_path_is_in_same_module_as_self_path ( ) {
497
+ assert ! ( target_path_is_in_same_module_as_self_path(
498
+ Utf8Path :: new( "index.js" ) ,
499
+ Utf8Path :: new( "self.js" )
500
+ ) ) ;
501
+ assert ! ( target_path_is_in_same_module_as_self_path(
502
+ Utf8Path :: new( "index.js" ) ,
503
+ Utf8Path :: new( "nested/self.js" )
504
+ ) ) ;
505
+ assert ! ( target_path_is_in_same_module_as_self_path(
506
+ Utf8Path :: new( "./index.js" ) ,
507
+ Utf8Path :: new( "./nested/self.js" )
508
+ ) ) ;
509
+ assert ! ( target_path_is_in_same_module_as_self_path(
510
+ Utf8Path :: new( "./nested/index.js" ) ,
511
+ Utf8Path :: new( "./nested/nested/self.js" )
512
+ ) ) ;
513
+
514
+ assert ! ( !target_path_is_in_same_module_as_self_path(
515
+ Utf8Path :: new( "target.js" ) ,
516
+ Utf8Path :: new( "self.js" )
517
+ ) ) ;
518
+ assert ! ( !target_path_is_in_same_module_as_self_path(
519
+ Utf8Path :: new( "target.js" ) ,
520
+ Utf8Path :: new( "nested/self.js" )
521
+ ) ) ;
522
+ assert ! ( !target_path_is_in_same_module_as_self_path(
523
+ Utf8Path :: new( "./target.js" ) ,
524
+ Utf8Path :: new( "./nested/self.js" )
525
+ ) ) ;
526
+ assert ! ( !target_path_is_in_same_module_as_self_path(
527
+ Utf8Path :: new( "target/index.js" ) ,
528
+ Utf8Path :: new( "self.js" )
529
+ ) ) ;
530
+ assert ! ( !target_path_is_in_same_module_as_self_path(
531
+ Utf8Path :: new( "target/index.js" ) ,
532
+ Utf8Path :: new( "nested/self.js" )
533
+ ) ) ;
534
+ assert ! ( !target_path_is_in_same_module_as_self_path(
535
+ Utf8Path :: new( "target/private.js" ) ,
536
+ Utf8Path :: new( "self.js" )
537
+ ) ) ;
538
+ assert ! ( !target_path_is_in_same_module_as_self_path(
539
+ Utf8Path :: new( "target/private.js" ) ,
540
+ Utf8Path :: new( "nested/self.js" )
541
+ ) ) ;
542
+ assert ! ( !target_path_is_in_same_module_as_self_path(
543
+ Utf8Path :: new( "./target/private.js" ) ,
544
+ Utf8Path :: new( "./self.js" )
545
+ ) ) ;
546
+ }
547
+
418
548
#[ test]
419
549
fn test_target_path_is_in_same_package_as_self_path ( ) {
550
+ assert ! ( target_path_is_in_same_package_as_self_path(
551
+ Utf8Path :: new( "index.js" ) ,
552
+ Utf8Path :: new( "self.js" )
553
+ ) ) ;
554
+ assert ! ( target_path_is_in_same_package_as_self_path(
555
+ Utf8Path :: new( "index.js" ) ,
556
+ Utf8Path :: new( "nested/self.js" )
557
+ ) ) ;
558
+ assert ! ( target_path_is_in_same_package_as_self_path(
559
+ Utf8Path :: new( "./index.js" ) ,
560
+ Utf8Path :: new( "./nested/self.js" )
561
+ ) ) ;
562
+ assert ! ( target_path_is_in_same_package_as_self_path(
563
+ Utf8Path :: new( "./nested/index.js" ) ,
564
+ Utf8Path :: new( "./nested/nested/self.js" )
565
+ ) ) ;
420
566
assert ! ( target_path_is_in_same_package_as_self_path(
421
567
Utf8Path :: new( "target.js" ) ,
422
568
Utf8Path :: new( "self.js" )
0 commit comments