2121import java .util .List ;
2222import java .util .Set ;
2323import java .util .regex .Pattern ;
24+ import java .util .stream .Stream ;
25+ import javax .annotation .Nullable ;
2426import org .sonar .check .Rule ;
2527import org .sonar .check .RuleProperty ;
2628import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
3335import org .sonar .plugins .python .api .tree .Name ;
3436import org .sonar .plugins .python .api .tree .SubscriptionExpression ;
3537import org .sonar .plugins .python .api .tree .Tree ;
38+ import org .sonar .plugins .python .api .types .v2 .PythonType ;
39+ import org .sonar .python .semantic .SymbolUtils ;
40+ import org .sonar .python .semantic .v2 .SymbolV2 ;
41+ import org .sonar .python .semantic .v2 .UsageV2 ;
42+ import org .sonar .python .tree .TreeUtils ;
43+ import org .sonar .python .types .v2 .TypeCheckBuilder ;
3644
3745@ Rule (key = "S117" )
3846public class LocalVariableAndParameterNameConventionCheck extends PythonSubscriptionCheck {
3947
4048 private static final String MESSAGE = "Rename this %s \" %s\" to match the regular expression %s." ;
4149 private static final String PARAMETER = "parameter" ;
4250 private static final String LOCAL_VAR = "local variable" ;
43- private static final EnumSet <Usage .Kind > USAGES = EnumSet .of (Usage .Kind .PARAMETER , Usage .Kind .LOOP_DECLARATION , Usage .Kind .ASSIGNMENT_LHS );
51+ private static final EnumSet <UsageV2 .Kind > USAGES = EnumSet .of (UsageV2 .Kind .PARAMETER , UsageV2 .Kind .LOOP_DECLARATION , UsageV2 .Kind .ASSIGNMENT_LHS );
4452
4553 private static final String CONSTANT_PATTERN = "^[_A-Z][A-Z0-9_]*$" ;
4654
@@ -54,17 +62,28 @@ public class LocalVariableAndParameterNameConventionCheck extends PythonSubscrip
5462 private Pattern pattern ;
5563 private static final Set <String > ML_VARIABLE_NAMES = Set .of ("X_train" , "X_test" , "Y_train" , "Y_test" , "X" , "Y" );
5664
65+ private TypeCheckBuilder isDjangoModelTypeCheck ;
66+
5767 @ Override
5868 public void initialize (Context context ) {
5969 pattern = Pattern .compile (format );
6070 constantPattern = Pattern .compile (CONSTANT_PATTERN );
61- context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , ctx -> {
62- FunctionDef funcDef = (FunctionDef ) ctx .syntaxNode ();
63- funcDef .localVariables ().stream ().sorted (Comparator .comparing (Symbol ::name )).forEach (s -> checkName (s , ctx ));
64- });
71+ context .registerSyntaxNodeConsumer (Tree .Kind .FILE_INPUT , this ::initializeTypeChecker );
72+ context .registerSyntaxNodeConsumer (Tree .Kind .FUNCDEF , this ::checkFunctionDef );
73+ }
74+
75+ private void initializeTypeChecker (SubscriptionContext ctx ) {
76+ isDjangoModelTypeCheck = ctx .typeChecker ().typeCheckBuilder ().isTypeOrInstanceWithName ("type" );
6577 }
6678
67- private void checkName (Symbol symbol , SubscriptionContext ctx ) {
79+ private void checkFunctionDef (SubscriptionContext ctx ) {
80+ FunctionDef funcDef = (FunctionDef ) ctx .syntaxNode ();
81+ TreeUtils .getLocalVariableSymbols (funcDef ).stream ()
82+ .sorted (Comparator .comparing (SymbolV2 ::name ))
83+ .forEach (s -> checkName (s , ctx ));
84+ }
85+
86+ private void checkName (SymbolV2 symbol , SubscriptionContext ctx ) {
6887 String name = symbol .name ();
6988 if (ML_VARIABLE_NAMES .contains (name )) {
7089 return ;
@@ -82,54 +101,69 @@ private void checkName(Symbol symbol, SubscriptionContext ctx) {
82101 }
83102 }
84103
85- private static boolean isType (Symbol symbol ) {
86- return isExtendingType (symbol ) || isAssignedFromTyping (symbol );
104+ private boolean isType (SymbolV2 symbolV2 ) {
105+ // TypeV1 and TypeV2 can detect different cases and work complementary to find more issues
106+ Symbol symbolV1 = SymbolUtils .symbolV2ToSymbolV1 (symbolV2 ).orElse (null );
107+ return symbolV1 != null && (isExtendingType (symbolV1 ) || isAssignedFromTyping (symbolV2 ) || isPythonTypeAClassType (symbolV2 ));
87108 }
88109
89110 private static boolean isExtendingType (Symbol symbol ) {
90- return symbol .usages ().stream ().map (Usage ::tree ).filter (Expression .class ::isInstance ).map (Expression .class ::cast ).anyMatch (e -> e .type ().mustBeOrExtend ("type" )) ||
91- (symbol .annotatedTypeName () != null && symbol .annotatedTypeName ().startsWith ("typing." ));
111+ boolean isInferredTypeExtendingType = symbol .usages ().stream ()
112+ .map (Usage ::tree )
113+ .filter (Expression .class ::isInstance )
114+ .map (Expression .class ::cast )
115+ .anyMatch (e -> e .type ().mustBeOrExtend ("type" ));
116+
117+ boolean isAnnotatedTypeStartingWithTyping = symbol .annotatedTypeName () != null && symbol .annotatedTypeName ().startsWith ("typing." );
118+ return isInferredTypeExtendingType || isAnnotatedTypeStartingWithTyping ;
92119 }
93120
94- private static boolean isAssignedFromTyping (Symbol symbol ) {
95- List <Tree > assignmentNames = symbol .usages ().stream ().filter (u -> u .kind () == Usage .Kind .ASSIGNMENT_LHS ).map (Usage ::tree ).toList ();
96- for (Tree assignmentName : assignmentNames ) {
97- Expression assignedValue = getAssignedValue (assignmentName );
98- if (assignedValue == null ) {
99- continue ;
100- }
101- if (assignedValue .is (Tree .Kind .SUBSCRIPTION )) {
102- SubscriptionExpression subscriptionExpression = (SubscriptionExpression ) assignedValue ;
103- if (subscriptionExpression .object ().is (Tree .Kind .NAME )) {
104- Symbol assignedSymbol = ((Name ) subscriptionExpression .object ()).symbol ();
105- if (assignedSymbol != null && isExtendingType (assignedSymbol )) {
106- return true ;
107- }
108- }
121+ private static boolean isAssignedFromTyping (SymbolV2 symbol ) {
122+ List <Expression > assignedValues = symbol .usages ().stream ()
123+ .filter (u -> u .kind () == UsageV2 .Kind .ASSIGNMENT_LHS )
124+ .flatMap (usage -> getAssignedValue (usage .tree ()))
125+ .toList ();
126+
127+ for (Expression assignedValue : assignedValues ) {
128+ Symbol assignedSymbol = getTypingSymbol (assignedValue );
129+ if (assignedSymbol != null && isExtendingType (assignedSymbol )) {
130+ return true ;
109131 }
110132 }
111133 return false ;
112134 }
113135
114- private static Expression getAssignedValue (Tree assignmentName ) {
115- while (assignmentName != null && !assignmentName .is (Tree .Kind .ASSIGNMENT_STMT )) {
116- assignmentName = assignmentName .parent ();
136+ private boolean isPythonTypeAClassType (SymbolV2 symbol ) {
137+ PythonType type = SymbolUtils .getPythonType (symbol );
138+ return isDjangoModelTypeCheck .check (type ).isTrue ();
139+ }
140+
141+ private static Stream <Expression > getAssignedValue (Tree assignmentName ) {
142+ var assignmentStmt = TreeUtils .firstAncestorOfClass (assignmentName , AssignmentStatement .class );
143+ if (assignmentStmt != null ) {
144+ return Stream .of (assignmentStmt .assignedValue ());
145+ } else {
146+ return Stream .empty ();
117147 }
118- if (assignmentName == null ) {
119- return null ;
148+ }
149+
150+ private static @ Nullable Symbol getTypingSymbol (Expression expr ) {
151+ if (expr instanceof SubscriptionExpression subscriptionExpression
152+ && subscriptionExpression .object () instanceof Name name ) {
153+ return name .symbol ();
120154 }
121- return (( AssignmentStatement ) assignmentName ). assignedValue () ;
155+ return null ;
122156 }
123157
124- private void raiseIssueForNameAndUsage (SubscriptionContext ctx , String name , Usage usage ) {
158+ private void raiseIssueForNameAndUsage (SubscriptionContext ctx , String name , UsageV2 usage ) {
125159 String type = PARAMETER ;
126- Usage .Kind kind = usage .kind ();
127- if (kind == Usage .Kind .ASSIGNMENT_LHS ) {
160+ UsageV2 .Kind kind = usage .kind ();
161+ if (kind == UsageV2 .Kind .ASSIGNMENT_LHS ) {
128162 type = LOCAL_VAR ;
129163 if (constantPattern .matcher (name ).matches ()) {
130164 return ;
131165 }
132- } else if (kind == Usage .Kind .LOOP_DECLARATION ) {
166+ } else if (kind == UsageV2 .Kind .LOOP_DECLARATION ) {
133167 type = LOCAL_VAR ;
134168 if (name .length () <= 1 ) {
135169 return ;
0 commit comments