From 7c65f17008e41ada04f8682f31f24b256b4fdb0a Mon Sep 17 00:00:00 2001
From: Guillaume Dequenne <guillaume.dequenne@sonarsource.com>
Date: Mon, 27 Jan 2025 09:37:18 +0100
Subject: [PATCH] SONARPY-2537 Reproduce changes

---
 .../python/semantic/v2/ReadUsagesVisitor.java |  8 ++-
 .../python/semantic/v2/TypeInferenceV2.java   |  4 +-
 .../org/sonar/python/semantic/v2/UsageV2.java |  1 +
 .../semantic/v2/TypeInferenceV2Test.java      | 52 +++++++++++++++++++
 4 files changed, 62 insertions(+), 3 deletions(-)

diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java b/python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java
index 40b2039353..c604fb44c7 100644
--- a/python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java
+++ b/python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java
@@ -139,7 +139,13 @@ private void addSymbolUsage(Name name) {
     var scope = currentScope();
     var symbol = scope.resolve(name.name());
     if (symbol != null && symbol.usages().stream().noneMatch(usage -> usage.tree().equals(name))) {
-      symbol.addUsage(name, UsageV2.Kind.OTHER);
+      if (name.parent().is(Tree.Kind.GLOBAL_STMT)) {
+        symbol.addUsage(name, UsageV2.Kind.GLOBAL_DECLARATION);
+      } else if (name.parent().is(Tree.Kind.NONLOCAL_STMT)) {
+        symbol.addUsage(name, UsageV2.Kind.NONLOCAL_DECLARATION);
+      } else {
+        symbol.addUsage(name, UsageV2.Kind.OTHER);
+      }
     }
   }
 }
diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/v2/TypeInferenceV2.java b/python-frontend/src/main/java/org/sonar/python/semantic/v2/TypeInferenceV2.java
index 69bb9dde00..bdd357e0a9 100644
--- a/python-frontend/src/main/java/org/sonar/python/semantic/v2/TypeInferenceV2.java
+++ b/python-frontend/src/main/java/org/sonar/python/semantic/v2/TypeInferenceV2.java
@@ -173,8 +173,8 @@ private static Set<SymbolV2> getTrackedVars(Set<SymbolV2> localVariables, Set<Na
       boolean hasMissingBindingUsage = variable.usages().stream()
         .filter(UsageV2::isBindingUsage)
         .anyMatch(u -> !assignedNames.contains(u.tree()));
-      boolean isGlobal = variable.usages().stream().anyMatch(v -> v.kind().equals(UsageV2.Kind.GLOBAL_DECLARATION));
-      if (!hasMissingBindingUsage && !isGlobal) {
+      boolean isGlobalOrNonLocal = variable.usages().stream().anyMatch(v -> v.kind().equals(UsageV2.Kind.GLOBAL_DECLARATION) || v.kind().equals(UsageV2.Kind.NONLOCAL_DECLARATION));
+      if (!hasMissingBindingUsage && !isGlobalOrNonLocal) {
         trackedVars.add(variable);
       }
     }
diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/v2/UsageV2.java b/python-frontend/src/main/java/org/sonar/python/semantic/v2/UsageV2.java
index cb213cb61e..79d488c823 100644
--- a/python-frontend/src/main/java/org/sonar/python/semantic/v2/UsageV2.java
+++ b/python-frontend/src/main/java/org/sonar/python/semantic/v2/UsageV2.java
@@ -40,6 +40,7 @@ public enum Kind {
     EXCEPTION_INSTANCE,
     WITH_INSTANCE,
     GLOBAL_DECLARATION,
+    NONLOCAL_DECLARATION,
     PATTERN_DECLARATION,
     TYPE_PARAM_DECLARATION,
     TYPE_ALIAS_DECLARATION,
diff --git a/python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java b/python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java
index 9ba3f70839..d933b5dcb2 100644
--- a/python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java
+++ b/python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java
@@ -1337,6 +1337,58 @@ void global_variable_builtin() {
       """).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
   }
 
+  @Test
+  void global_variable_in_nested_function() {
+    FileInput fileInput = inferTypes("""
+      def outer():
+        a = 24
+        def nested():
+          global a
+        a
+      """);
+    var outerFunctionDef = (FunctionDef) fileInput.statements().statements().get(0);
+    SymbolV2 symbolV2 = ((Name) ((ExpressionStatement) outerFunctionDef.body().statements().get(2)).expressions().get(0)).symbolV2();
+    assertThat(symbolV2.usages()).extracting(UsageV2::kind).containsExactlyInAnyOrder(UsageV2.Kind.ASSIGNMENT_LHS, UsageV2.Kind.GLOBAL_DECLARATION, UsageV2.Kind.OTHER);
+  }
+
+  @Test
+  void nonlocal_variable_in_nested_function() {
+    FileInput fileInput = inferTypes("""
+      def outer():
+        a = 24
+        def nested():
+          nonlocal a
+        a
+      """);
+    var outerFunctionDef = (FunctionDef) fileInput.statements().statements().get(0);
+    SymbolV2 symbolV2 = ((Name) ((ExpressionStatement) outerFunctionDef.body().statements().get(2)).expressions().get(0)).symbolV2();
+    assertThat(symbolV2.usages()).extracting(UsageV2::kind).containsExactlyInAnyOrder(UsageV2.Kind.ASSIGNMENT_LHS, UsageV2.Kind.NONLOCAL_DECLARATION, UsageV2.Kind.OTHER);
+  }
+
+  @Test
+  void nonlocal_variable_try_except() {
+    FileInput fileInput = inferTypes("""
+      def outer():
+          contains_target = True
+            
+          def nested(item):
+              nonlocal contains_target
+              if cond():
+                  contains_target = contains_target and foo()
+            
+          try:
+              return contains_target
+          except Exception as e:
+              ...
+          contains_target
+      """);
+    var outerFunctionDef = (FunctionDef) fileInput.statements().statements().get(0);
+    SymbolV2 symbolV2 = ((Name) ((ExpressionStatement) outerFunctionDef.body().statements().get(3)).expressions().get(0)).symbolV2();
+    assertThat(symbolV2.usages()).extracting(UsageV2::kind).containsExactlyInAnyOrder(
+      UsageV2.Kind.ASSIGNMENT_LHS, UsageV2.Kind.NONLOCAL_DECLARATION, UsageV2.Kind.ASSIGNMENT_LHS, UsageV2.Kind.OTHER, UsageV2.Kind.OTHER, UsageV2.Kind.OTHER
+    );
+  }
+
   @Test
   void conditional_assignment() {
     PythonType type = lastExpression("""