Skip to content

Commit 582f26d

Browse files
SONARPY-672e Improve issue reporting for S5797 (#741)
* Add ClassSymbol#definitionLocation API * SONARPY-672 Improve issue reporting for S5797
1 parent 7f84975 commit 582f26d

File tree

9 files changed

+80
-21
lines changed

9 files changed

+80
-21
lines changed

python-checks/src/main/java/org/sonar/python/checks/ConstantConditionCheck.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import java.util.List;
2424
import java.util.Set;
2525
import org.sonar.check.Rule;
26+
import org.sonar.plugins.python.api.LocationInFile;
2627
import org.sonar.plugins.python.api.PythonVisitorCheck;
2728
import org.sonar.plugins.python.api.symbols.AmbiguousSymbol;
29+
import org.sonar.plugins.python.api.symbols.ClassSymbol;
2830
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
2931
import org.sonar.plugins.python.api.symbols.Symbol;
3032
import org.sonar.plugins.python.api.tree.BinaryExpression;
@@ -182,7 +184,7 @@ private void checkExpression(Expression expression) {
182184
if (expression.is(NAME) || expression.is(QUALIFIED_EXPR)) {
183185
Symbol symbol = ((HasSymbol) expression).symbol();
184186
if (symbol != null && isClassOrFunction(symbol)) {
185-
addIssue(expression, MESSAGE);
187+
raiseIssueOnClassOrFunction(expression, symbol);
186188
return;
187189
}
188190
}
@@ -197,6 +199,15 @@ private void checkExpression(Expression expression) {
197199
}
198200
}
199201

202+
private void raiseIssueOnClassOrFunction(Expression expression, Symbol symbol) {
203+
PreciseIssue issue = addIssue(expression, MESSAGE);
204+
LocationInFile locationInFile = locationForClassOrFunction(symbol);
205+
if (locationInFile != null) {
206+
String type = symbol.is(Symbol.Kind.CLASS) ? "Class" : "Function";
207+
issue.secondary(locationInFile, String.format("%s definition.", type));
208+
}
209+
}
210+
200211
private static boolean isClassOrFunction(Symbol symbol) {
201212
if (symbol.is(Symbol.Kind.CLASS)) {
202213
return true;
@@ -205,6 +216,10 @@ private static boolean isClassOrFunction(Symbol symbol) {
205216
// Avoid potential FPs with properties: only report on limited selection of "safe" decorators
206217
return ACCEPTED_DECORATORS.containsAll(((FunctionSymbol) symbol).decorators());
207218
}
208-
return symbol.is(Symbol.Kind.AMBIGUOUS) && ((AmbiguousSymbol) symbol).alternatives().stream().allMatch(ConstantConditionCheck::isClassOrFunction);
219+
return false;
220+
}
221+
222+
private static LocationInFile locationForClassOrFunction(Symbol symbol) {
223+
return symbol.is(Symbol.Kind.CLASS) ? ((ClassSymbol) symbol).definitionLocation() : ((FunctionSymbol) symbol).definitionLocation();
209224
}
210225
}

python-checks/src/test/resources/checks/constantCondition.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,14 @@ def myfunction():
152152

153153
if myfunction: # Noncompliant
154154
pass
155-
elif round: # Noncompliant
155+
elif round: # FN (ambiguous symbol)
156+
pass
157+
elif float.__add__: # Noncompliant
156158
pass
157159

158160
def class_and_methods():
159161
class MyClass:
162+
# ^^^^^^^> {{Class definition.}}
160163
def mymethod(self): ...
161164
@staticmethod
162165
def mystaticmethod(): ...
@@ -169,7 +172,8 @@ def othercachedproperty(self): ...
169172

170173
myinstance = MyClass()
171174

172-
if MyClass: ... # Noncompliant
175+
if MyClass: ... # Noncompliant {{Replace this expression; used as a condition it will always be constant.}}
176+
# ^^^^^^^
173177
elif MyClass.mymethod: ... # Noncompliant
174178
elif myinstance.mymethod: ... # Noncompliant
175179
elif myinstance.mystaticmethod: ... # Noncompliant
@@ -182,7 +186,7 @@ def ambiguous_symbols():
182186
class ambiguous_class_or_function: ...
183187
else:
184188
def ambiguous_class_or_function(): ...
185-
if ambiguous_class_or_function: ... # Noncompliant
189+
if ambiguous_class_or_function: ... # OK
186190

187191
if cond():
188192
class true_ambiguous: ...

python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/ClassSymbol.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.List;
2424
import java.util.Optional;
2525
import java.util.Set;
26+
import javax.annotation.CheckForNull;
27+
import org.sonar.plugins.python.api.LocationInFile;
2628

2729
public interface ClassSymbol extends Symbol {
2830
List<Symbol> superClasses();
@@ -31,6 +33,9 @@ public interface ClassSymbol extends Symbol {
3133

3234
Set<Symbol> declaredMembers();
3335

36+
@CheckForNull
37+
LocationInFile definitionLocation();
38+
3439
@Beta
3540
Optional<Symbol> resolveMember(String memberName);
3641

python-frontend/src/main/java/org/sonar/python/semantic/ClassSymbolImpl.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.sonar.python.semantic;
2121

2222

23+
import java.nio.file.Path;
2324
import java.util.ArrayList;
2425
import java.util.Collection;
2526
import java.util.Collections;
@@ -32,9 +33,15 @@
3233
import java.util.Set;
3334
import java.util.stream.Collectors;
3435
import javax.annotation.Nullable;
36+
import org.sonar.plugins.python.api.LocationInFile;
37+
import org.sonar.plugins.python.api.PythonFile;
3538
import org.sonar.plugins.python.api.symbols.AmbiguousSymbol;
3639
import org.sonar.plugins.python.api.symbols.ClassSymbol;
3740
import org.sonar.plugins.python.api.symbols.Symbol;
41+
import org.sonar.plugins.python.api.tree.ClassDef;
42+
43+
import static org.sonar.python.semantic.SymbolUtils.pathOf;
44+
import static org.sonar.python.tree.TreeUtils.locationInFile;
3845

3946
public class ClassSymbolImpl extends SymbolImpl implements ClassSymbol {
4047

@@ -46,15 +53,32 @@ public class ClassSymbolImpl extends SymbolImpl implements ClassSymbol {
4653
private Map<String, Symbol> membersByName = null;
4754
private boolean hasAlreadyReadSuperClasses = false;
4855
private boolean hasAlreadyReadMembers = false;
56+
private final LocationInFile classDefinitionLocation;
57+
58+
public ClassSymbolImpl(ClassDef classDef, @Nullable String fullyQualifiedName, PythonFile pythonFile) {
59+
super(classDef.name().name(), fullyQualifiedName);
60+
this.setKind(Kind.CLASS);
61+
String fileId = null;
62+
if (!SymbolUtils.isTypeShedFile(pythonFile)) {
63+
Path path = pathOf(pythonFile);
64+
fileId = path != null ? path.toString() : pythonFile.toString();
65+
}
66+
classDefinitionLocation = locationInFile(classDef.name(), fileId);
67+
}
4968

5069
public ClassSymbolImpl(String name, @Nullable String fullyQualifiedName) {
70+
this(name, fullyQualifiedName, null);
71+
}
72+
73+
public ClassSymbolImpl(String name, @Nullable String fullyQualifiedName, @Nullable LocationInFile definitionLocation) {
5174
super(name, fullyQualifiedName);
52-
this.setKind(Kind.CLASS);
75+
classDefinitionLocation = definitionLocation;
76+
setKind(Kind.CLASS);
5377
}
5478

5579
@Override
5680
ClassSymbolImpl copyWithoutUsages() {
57-
ClassSymbolImpl copiedClassSymbol = new ClassSymbolImpl(name(), fullyQualifiedName());
81+
ClassSymbolImpl copiedClassSymbol = new ClassSymbolImpl(name(), fullyQualifiedName(), definitionLocation());
5882
for (Symbol superClass : superClasses()) {
5983
if (superClass == this) {
6084
copiedClassSymbol.superClasses.add(copiedClassSymbol);
@@ -123,6 +147,11 @@ public Optional<Symbol> resolveMember(String memberName) {
123147
return Optional.empty();
124148
}
125149

150+
@Override
151+
public LocationInFile definitionLocation() {
152+
return classDefinitionLocation;
153+
}
154+
126155
@Override
127156
public boolean isOrExtends(String fullyQualifiedClassName) {
128157
return allSuperClasses(false).stream().anyMatch(c -> c.fullyQualifiedName() != null && Objects.equals(fullyQualifiedClassName, c.fullyQualifiedName()));

python-frontend/src/main/java/org/sonar/python/semantic/FunctionSymbolImpl.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@
3939
import org.sonar.plugins.python.api.tree.Tree;
4040
import org.sonar.plugins.python.api.tree.TypeAnnotation;
4141
import org.sonar.plugins.python.api.types.InferredType;
42-
import org.sonar.python.TokenLocation;
4342
import org.sonar.python.types.InferredTypes;
4443

4544
import static org.sonar.python.semantic.SymbolUtils.pathOf;
45+
import static org.sonar.python.tree.TreeUtils.locationInFile;
4646

4747
public class FunctionSymbolImpl extends SymbolImpl implements FunctionSymbol {
4848
private final List<Parameter> parameters = new ArrayList<>();
@@ -102,16 +102,6 @@ public FunctionSymbolImpl(String name, @Nullable String fullyQualifiedName, bool
102102
this.isStub = true;
103103
}
104104

105-
@CheckForNull
106-
private static LocationInFile locationInFile(Tree tree, @Nullable String fileId) {
107-
if (fileId == null) {
108-
return null;
109-
}
110-
TokenLocation firstToken = new TokenLocation(tree.firstToken());
111-
TokenLocation lastToken = new TokenLocation(tree.lastToken());
112-
return new LocationInFile(fileId, firstToken.startLine(), firstToken.startLineOffset(), lastToken.endLine(), lastToken.endLineOffset());
113-
}
114-
115105
@Override
116106
FunctionSymbolImpl copyWithoutUsages() {
117107
FunctionSymbolImpl copy = new FunctionSymbolImpl(name(), this);

python-frontend/src/main/java/org/sonar/python/semantic/Scope.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.sonar.python.tree.FunctionDefImpl;
4242
import org.sonar.python.types.InferredTypes;
4343
import org.sonar.python.types.TypeShed;
44+
4445
import static org.sonar.python.semantic.SymbolUtils.isTypeShedFile;
4546

4647
class Scope {
@@ -129,8 +130,9 @@ private static Symbol copySymbol(String symbolName, Symbol symbol, Map<String, S
129130
if (symbol.is(Symbol.Kind.FUNCTION)) {
130131
return new FunctionSymbolImpl(symbolName, (FunctionSymbol) symbol);
131132
} else if (symbol.is(Symbol.Kind.CLASS)) {
132-
ClassSymbolImpl classSymbol = new ClassSymbolImpl(symbolName, symbol.fullyQualifiedName());
133133
ClassSymbolImpl originalClassSymbol = (ClassSymbolImpl) symbol;
134+
// Must use symbolName to preserve import aliases
135+
ClassSymbolImpl classSymbol = new ClassSymbolImpl(symbolName, originalClassSymbol.fullyQualifiedName(), originalClassSymbol.definitionLocation());
134136
for (Symbol originalSymbol : originalClassSymbol.superClasses()) {
135137
Symbol globalSymbol = globalSymbolsByFQN.get(originalSymbol.fullyQualifiedName());
136138
if (globalSymbol != null && globalSymbol.kind() == Symbol.Kind.CLASS) {
@@ -254,7 +256,7 @@ void addClassSymbol(ClassDef classDef, @Nullable String fullyQualifiedName) {
254256
if (isExistingSymbol(symbolName)) {
255257
addBindingUsage(classDef.name(), Usage.Kind.CLASS_DECLARATION, fullyQualifiedName);
256258
} else {
257-
ClassSymbolImpl classSymbol = new ClassSymbolImpl(symbolName, fullyQualifiedName);
259+
ClassSymbolImpl classSymbol = new ClassSymbolImpl(classDef, fullyQualifiedName, pythonFile);
258260
symbols.add(classSymbol);
259261
symbolsByName.put(symbolName, classSymbol);
260262
classSymbol.addUsage(classDef.name(), Usage.Kind.CLASS_DECLARATION);

python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ private Set<Symbol> getAlternativeDefinitions(Symbol symbol, List<Usage> binding
188188
alternativeDefinitions.add(functionSymbol);
189189
break;
190190
case CLASS_DECLARATION:
191-
ClassSymbolImpl classSymbol = new ClassSymbolImpl(symbol.name(), symbol.fullyQualifiedName());
192191
ClassDef classDef = (ClassDef) bindingUsage.tree().parent();
192+
ClassSymbolImpl classSymbol = new ClassSymbolImpl(classDef, symbol.fullyQualifiedName(), pythonFile);
193193
resolveTypeHierarchy(classDef, classSymbol, pythonFile, scopesByRootTree.get(fileInput).symbolsByName);
194194
Scope classScope = scopesByRootTree.get(classDef);
195195
classSymbol.addMembers(getClassMembers(classScope.symbolsByName, classScope.instanceAttributesByName));

python-frontend/src/main/java/org/sonar/python/tree/TreeUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.stream.Stream;
3131
import javax.annotation.CheckForNull;
3232
import javax.annotation.Nullable;
33+
import org.sonar.plugins.python.api.LocationInFile;
3334
import org.sonar.plugins.python.api.symbols.ClassSymbol;
3435
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
3536
import org.sonar.plugins.python.api.symbols.Symbol;
@@ -48,6 +49,7 @@
4849
import org.sonar.plugins.python.api.tree.Tree;
4950
import org.sonar.plugins.python.api.tree.Tree.Kind;
5051
import org.sonar.plugins.python.api.tree.Tuple;
52+
import org.sonar.python.TokenLocation;
5153
import org.sonar.python.api.PythonTokenType;
5254

5355
public class TreeUtils {
@@ -252,4 +254,14 @@ public static boolean isBooleanLiteral(Tree tree) {
252254
}
253255
return false;
254256
}
257+
258+
@CheckForNull
259+
public static LocationInFile locationInFile(Tree tree, @Nullable String fileId) {
260+
if (fileId == null) {
261+
return null;
262+
}
263+
TokenLocation firstToken = new TokenLocation(tree.firstToken());
264+
TokenLocation lastToken = new TokenLocation(tree.lastToken());
265+
return new LocationInFile(fileId, firstToken.startLine(), firstToken.startLineOffset(), lastToken.endLine(), lastToken.endLineOffset());
266+
}
255267
}

python-frontend/src/test/java/org/sonar/python/semantic/FullyQualifiedNameTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
package org.sonar.python.semantic;
2121

22+
import java.net.URI;
2223
import javax.annotation.Nullable;
2324
import org.junit.Test;
2425
import org.mockito.Mockito;
@@ -429,6 +430,7 @@ public void init_module_relative_import() {
429430
FileInput fileInput = new PythonTreeMaker().fileInput(PythonParser.create().parse(code));
430431
PythonFile pythonFile = Mockito.mock(PythonFile.class, "__init__.py");
431432
when(pythonFile.fileName()).thenReturn("__init__.py");
433+
when(pythonFile.uri()).thenReturn(URI.create("mod/__init__.py"));
432434
PythonVisitorContext context = new PythonVisitorContext(fileInput, pythonFile, null, "foo.bar");
433435
fileInput = context.rootTree();
434436
CallExpression callExpression = (CallExpression) getAllDescendant(fileInput, tree -> tree.is(Tree.Kind.CALL_EXPR)).get(0);

0 commit comments

Comments
 (0)