@@ -20,13 +20,15 @@ fn evaluate_module_rule_with(
2020 rule : & ModuleRule ,
2121 evaluate_leaf : & impl Fn ( & ModuleRule ) -> Option < bool > ,
2222) -> Option < bool > {
23+ // If either operand doesn't apply to this item kind, the whole composite
24+ // rule is inapplicable rather than silently collapsing to whichever
25+ // operand happened to apply.
2326 let combine =
2427 |left : Option < bool > , right : Option < bool > , operation : fn ( bool , bool ) -> bool | match (
2528 left, right,
2629 ) {
2730 ( Some ( left) , Some ( right) ) => Some ( operation ( left, right) ) ,
28- ( Some ( result) , None ) | ( None , Some ( result) ) => Some ( result) ,
29- ( None , None ) => None ,
31+ _ => None ,
3032 } ;
3133
3234 match rule {
@@ -47,6 +49,53 @@ fn evaluate_module_rule_with(
4749 }
4850}
4951
52+ // Walks a composite rule that evaluated to `Some(false)`, collecting the
53+ // individual leaves responsible along with the boolean value each one was
54+ // expected to produce. Mirrors the struct-lint equivalent so And/Or/Not
55+ // failures report distinct, leaf-specific diagnostics instead of one generic
56+ // message. Only ever called once the root rule has evaluated to `Some`, so
57+ // every leaf reached here is guaranteed to evaluate to `Some` too (a `None`
58+ // leaf would have made the whole composite `None`, not `Some(false)`).
59+ fn collect_module_rule_violations < ' a > (
60+ rule : & ' a ModuleRule ,
61+ expected : bool ,
62+ evaluate_leaf : & impl Fn ( & ModuleRule ) -> Option < bool > ,
63+ violations : & mut Vec < ( & ' a ModuleRule , bool ) > ,
64+ ) {
65+ let actual = evaluate_module_rule_with ( rule, evaluate_leaf) . expect (
66+ "collect_module_rule_violations is only invoked once the root rule evaluates to Some" ,
67+ ) ;
68+ if actual == expected {
69+ return ;
70+ }
71+
72+ match rule {
73+ ModuleRule :: And ( left, right) if expected => {
74+ collect_module_rule_violations ( left, true , evaluate_leaf, violations) ;
75+ collect_module_rule_violations ( right, true , evaluate_leaf, violations) ;
76+ }
77+ ModuleRule :: And ( left, _right) => {
78+ // Both operands are true when !(left && right) fails, but only
79+ // one needs to become false to satisfy the negation - report a
80+ // single candidate rather than presenting both as required fixes.
81+ collect_module_rule_violations ( left, false , evaluate_leaf, violations) ;
82+ }
83+ ModuleRule :: Or ( left, right) if expected => {
84+ collect_module_rule_violations ( left, true , evaluate_leaf, violations) ;
85+ collect_module_rule_violations ( right, true , evaluate_leaf, violations) ;
86+ }
87+ ModuleRule :: Or ( left, right) => {
88+ collect_module_rule_violations ( left, false , evaluate_leaf, violations) ;
89+ collect_module_rule_violations ( right, false , evaluate_leaf, violations) ;
90+ }
91+ ModuleRule :: Not ( inner) => {
92+ collect_module_rule_violations ( inner, !expected, evaluate_leaf, violations) ;
93+ }
94+ leaf => violations. push ( ( leaf, expected) ) ,
95+ }
96+ }
97+
98+ #[ cfg( test) ]
5099fn module_rule_severity ( rule : & ModuleRule ) -> Severity {
51100 match rule {
52101 ModuleRule :: MustBeNamed ( _, severity)
@@ -334,6 +383,162 @@ impl ModuleLint {
334383 }
335384 }
336385
386+ // Renders a leaf-specific diagnostic for one violation collected by
387+ // `collect_module_rule_violations`, reusing the same lint declarations
388+ // and message styles as the direct (non-logical) rule branches below.
389+ fn render_module_rule_violation (
390+ & self ,
391+ ctx : & LateContext < ' _ > ,
392+ item : & Item < ' _ > ,
393+ rule : & ModuleRule ,
394+ expected : bool ,
395+ ) {
396+ match rule {
397+ ModuleRule :: MustBeNamed ( pattern, severity)
398+ | ModuleRule :: MustNotBeNamed ( pattern, severity) => {
399+ let must_match = matches ! ( rule, ModuleRule :: MustBeNamed ( ..) ) == expected;
400+ let item_name = ctx
401+ . tcx
402+ . item_name ( item. owner_id . def_id . to_def_id ( ) )
403+ . to_string ( ) ;
404+ if must_match {
405+ span_lint_and_help (
406+ ctx,
407+ MODULE_MUST_BE_NAMED :: get_by_severity ( * severity) ,
408+ self . name ( ) . as_str ( ) ,
409+ item. span ,
410+ format ! ( "Module must match pattern '{pattern}', found '{item_name}'" ) ,
411+ None ,
412+ format ! ( "Rename this module to match the pattern '{pattern}'" ) ,
413+ ) ;
414+ } else {
415+ span_lint_and_help (
416+ ctx,
417+ MODULE_MUST_NOT_BE_NAMED :: get_by_severity ( * severity) ,
418+ self . name ( ) . as_str ( ) ,
419+ item. span ,
420+ format ! ( "Module must not match pattern '{pattern}'" ) ,
421+ None ,
422+ "Choose a name that doesn't match this pattern" ,
423+ ) ;
424+ }
425+ }
426+ ModuleRule :: MustNotBeEmpty ( severity) => {
427+ if expected {
428+ span_lint_and_help (
429+ ctx,
430+ MODULE_MUST_NOT_BE_EMPTY :: get_by_severity ( * severity) ,
431+ self . name ( ) . as_str ( ) ,
432+ item. span ,
433+ "Module must not be empty" ,
434+ None ,
435+ "Add content to this module or remove it" ,
436+ ) ;
437+ } else {
438+ span_lint_and_help (
439+ ctx,
440+ MODULE_MUST_BE_EMPTY :: get_by_severity ( * severity) ,
441+ self . name ( ) . as_str ( ) ,
442+ item. span ,
443+ "Module must be empty" ,
444+ None ,
445+ "Remove content from this module" ,
446+ ) ;
447+ }
448+ }
449+ ModuleRule :: MustBeEmpty ( severity) => {
450+ if expected {
451+ span_lint_and_help (
452+ ctx,
453+ MODULE_MUST_BE_EMPTY :: get_by_severity ( * severity) ,
454+ self . name ( ) . as_str ( ) ,
455+ item. span ,
456+ "Module must be empty" ,
457+ None ,
458+ "Remove disallowed items from this module" ,
459+ ) ;
460+ } else {
461+ span_lint_and_help (
462+ ctx,
463+ MODULE_MUST_NOT_BE_EMPTY :: get_by_severity ( * severity) ,
464+ self . name ( ) . as_str ( ) ,
465+ item. span ,
466+ "Module must not be empty" ,
467+ None ,
468+ "Add content to this module" ,
469+ ) ;
470+ }
471+ }
472+ ModuleRule :: MustHaveEmptyModFile ( severity) => {
473+ let message = if expected {
474+ "Module's mod.rs file must be empty (only allowed to re-export other modules)"
475+ } else {
476+ "Module's mod.rs file must not be empty"
477+ } ;
478+ span_lint_and_help (
479+ ctx,
480+ MODULE_MUST_HAVE_EMPTY_MOD_FILE :: get_by_severity ( * severity) ,
481+ self . name ( ) . as_str ( ) ,
482+ item. span ,
483+ message,
484+ None ,
485+ "Remove disallowed items from the mod.rs file or move them to a submodule" ,
486+ ) ;
487+ }
488+ ModuleRule :: RestrictImports { severity, .. } => {
489+ let message = if expected {
490+ "Use of this module is not permitted by the configured import restrictions"
491+ } else {
492+ "Use of this module is required to be denied by the configured import restrictions"
493+ } ;
494+ span_lint_and_help (
495+ ctx,
496+ MODULE_RESTRICT_IMPORTS :: get_by_severity ( * severity) ,
497+ self . name ( ) . as_str ( ) ,
498+ item. span ,
499+ message,
500+ None ,
501+ "Adjust this import to satisfy the configured restrictions" ,
502+ ) ;
503+ }
504+ ModuleRule :: NoWildcardImports ( severity) => {
505+ let message = if expected {
506+ "Wildcard imports are not allowed"
507+ } else {
508+ "Wildcard imports are required here"
509+ } ;
510+ span_lint_and_help (
511+ ctx,
512+ MODULE_WILDCARD_IMPORT :: get_by_severity ( * severity) ,
513+ self . name ( ) . as_str ( ) ,
514+ item. span ,
515+ message,
516+ None ,
517+ "Import specific items instead of using a wildcard" ,
518+ ) ;
519+ }
520+ ModuleRule :: DeniedItems { severity, .. } => {
521+ let message = if expected {
522+ "This item type is not allowed in this module"
523+ } else {
524+ "This item type is required to be denied in this module"
525+ } ;
526+ span_lint_and_help (
527+ ctx,
528+ MODULE_DENIED_ITEMS :: get_by_severity ( * severity) ,
529+ self . name ( ) . as_str ( ) ,
530+ item. span ,
531+ message,
532+ None ,
533+ "Consider moving this item to a different module" ,
534+ ) ;
535+ }
536+ ModuleRule :: And ( _, _) | ModuleRule :: Or ( _, _) | ModuleRule :: Not ( _) => {
537+ unreachable ! ( "composite rules are not leaves" )
538+ }
539+ }
540+ }
541+
337542 // Helper function to check for disallowed items in a module and call the callback when found
338543 fn check_for_disallowed_items < C > (
339544 & self ,
@@ -435,14 +640,6 @@ declare_variable_severity_lint!(
435640 "Module's mod.rs file must be empty (only allowed to re-export other modules)"
436641) ;
437642
438- declare_variable_severity_lint ! (
439- pub ,
440- MODULE_LOGICAL_RULE ,
441- MODULE_LOGICAL_RULE_LINT_DENY ,
442- MODULE_LOGICAL_RULE_LINT_WARN ,
443- "Module must satisfy its configured logical rule"
444- ) ;
445-
446643impl_lint_pass ! ( ModuleLint => [
447644 MODULE_MUST_BE_NAMED_LINT_DENY , MODULE_MUST_BE_NAMED_LINT_WARN ,
448645 MODULE_MUST_NOT_BE_NAMED_LINT_DENY , MODULE_MUST_NOT_BE_NAMED_LINT_WARN ,
@@ -451,8 +648,7 @@ impl_lint_pass!(ModuleLint => [
451648 MODULE_MUST_HAVE_EMPTY_MOD_FILE_LINT_DENY , MODULE_MUST_HAVE_EMPTY_MOD_FILE_LINT_WARN ,
452649 MODULE_RESTRICT_IMPORTS_LINT_DENY , MODULE_RESTRICT_IMPORTS_LINT_WARN ,
453650 MODULE_WILDCARD_IMPORT_LINT_DENY , MODULE_WILDCARD_IMPORT_LINT_WARN ,
454- MODULE_DENIED_ITEMS_LINT_DENY , MODULE_DENIED_ITEMS_LINT_WARN ,
455- MODULE_LOGICAL_RULE_LINT_DENY , MODULE_LOGICAL_RULE_LINT_WARN
651+ MODULE_DENIED_ITEMS_LINT_DENY , MODULE_DENIED_ITEMS_LINT_WARN
456652] ) ;
457653
458654impl ArchitectureLintRule for ModuleLint {
@@ -730,16 +926,14 @@ impl<'tcx> LateLintPass<'tcx> for ModuleLint {
730926 }
731927 }
732928 ModuleRule :: And ( _, _) | ModuleRule :: Or ( _, _) | ModuleRule :: Not ( _) => {
929+ let evaluate_leaf =
930+ |leaf : & ModuleRule | self . evaluate_module_rule_leaf ( ctx, item, leaf) ;
733931 if self . evaluate_module_rule ( ctx, item, rule) == Some ( false ) {
734- span_lint_and_help (
735- ctx,
736- MODULE_LOGICAL_RULE :: get_by_severity ( module_rule_severity ( rule) ) ,
737- self . name ( ) . as_str ( ) ,
738- item. span ,
739- "Item does not satisfy the configured logical module rule" ,
740- None ,
741- "Satisfy at least the required combination of module constraints" ,
742- ) ;
932+ let mut violations = Vec :: new ( ) ;
933+ collect_module_rule_violations ( rule, true , & evaluate_leaf, & mut violations) ;
934+ for ( leaf, expected) in violations {
935+ self . render_module_rule_violation ( ctx, item, leaf, expected) ;
936+ }
743937 }
744938 }
745939 }
@@ -790,13 +984,13 @@ mod tests {
790984 }
791985
792986 #[ test]
793- fn ignores_inapplicable_leaves_without_hiding_applicable_results ( ) {
987+ fn inapplicable_leaf_makes_composite_inapplicable ( ) {
794988 let rule = ModuleRule :: And (
795989 Box :: new ( named ( "passes" , Severity :: Warn ) ) ,
796990 Box :: new ( ModuleRule :: MustNotBeEmpty ( Severity :: Warn ) ) ,
797991 ) ;
798992
799- assert_eq ! ( evaluate_module_rule_with( & rule, & leaf_value) , Some ( true ) ) ;
993+ assert_eq ! ( evaluate_module_rule_with( & rule, & leaf_value) , None ) ;
800994 }
801995
802996 #[ test]
0 commit comments