2323import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2424import org .sonar .plugins .python .api .PythonVersionUtils ;
2525import org .sonar .plugins .python .api .SubscriptionContext ;
26- import org .sonar .plugins .python .api .symbols .Symbol ;
2726import org .sonar .plugins .python .api .tree .AliasedName ;
2827import org .sonar .plugins .python .api .tree .DottedName ;
2928import org .sonar .plugins .python .api .tree .ImportFrom ;
3029import org .sonar .plugins .python .api .tree .ImportName ;
3130import org .sonar .plugins .python .api .tree .Tree ;
31+ import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatcher ;
32+ import org .sonar .plugins .python .api .types .v2 .matchers .TypeMatchers ;
3233import org .sonar .python .tree .TreeUtils ;
3334
3435@ Rule (key = "S6890" )
3536public class PytzUsageCheck extends PythonSubscriptionCheck {
3637 private static final String MESSAGE = "Don't use `pytz` module with Python 3.9 and later." ;
38+ private static final TypeMatcher PYTZ_PREFIX_MATCHER = TypeMatchers .withFQNPrefix ("pytz." );
3739
3840 @ Override
3941 public void initialize (Context context ) {
@@ -50,10 +52,19 @@ private static void checkImport(SubscriptionContext context) {
5052 return ;
5153 }
5254
55+ if (context .syntaxNode () instanceof ImportFrom importFrom ) {
56+ if (importFrom .dottedPrefixForModule ().isEmpty ()) {
57+ Optional .ofNullable (importFrom .module ())
58+ // V1 associated the imported symbol with the alias rather than the imported name.
59+ // Preserve that behavior: an aliased "from pytz import ..." import is not reported.
60+ .ifPresent (module -> importFrom .importedNames ().stream ()
61+ .filter (name -> name .alias () == null && (isPytzModule (module ) || isPytzImportedName (name )))
62+ .forEach (name -> context .addIssue (module , MESSAGE )));
63+ }
64+ return ;
65+ }
66+
5367 Stream .of (
54- Optional .of (context .syntaxNode ())
55- .flatMap (TreeUtils .toOptionalInstanceOfMapper (ImportFrom .class ))
56- .map (ImportFrom ::importedNames ),
5768 Optional .of (context .syntaxNode ())
5869 .flatMap (TreeUtils .toOptionalInstanceOfMapper (ImportName .class ))
5970 .map (ImportName ::modules ))
@@ -65,18 +76,17 @@ private static void checkImport(SubscriptionContext context) {
6576 .filter (list -> !list .isEmpty ())
6677 .map (names -> names .get (0 ))
6778 .filter (name -> "pytz" .equals (name .name ())
68- || Optional .ofNullable (name .symbol ())
69- .map (Symbol ::fullyQualifiedName )
70- .filter (fqn -> fqn .startsWith ("pytz" )).isPresent ())
71- .forEach (name -> raiseIssue (context , name ));
79+ || PYTZ_PREFIX_MATCHER .isTrueFor (name , context ))
80+ .forEach (name -> context .addIssue (name , MESSAGE ));
7281 }
7382
74- private static void raiseIssue (SubscriptionContext context , Tree tree ) {
75- if (context .syntaxNode ().is (Tree .Kind .IMPORT_FROM )) {
76- ImportFrom importFrom = (ImportFrom ) context .syntaxNode ();
77- Optional .ofNullable (importFrom .module ()).ifPresent (module -> context .addIssue (module , MESSAGE ));
78- } else {
79- context .addIssue (tree , MESSAGE );
80- }
83+ private static boolean isPytzModule (DottedName module ) {
84+ return !module .names ().isEmpty ()
85+ && "pytz" .equals (module .names ().get (0 ).name ());
86+ }
87+
88+ private static boolean isPytzImportedName (AliasedName name ) {
89+ return !name .dottedName ().names ().isEmpty ()
90+ && "pytz" .equals (name .dottedName ().names ().get (0 ).name ());
8191 }
8292}
0 commit comments