diff --git a/its/ruling/src/test/resources/expected/python-S116.json b/its/ruling/src/test/resources/expected/python-S116.json index 6d2e048e7..d61f25e53 100644 --- a/its/ruling/src/test/resources/expected/python-S116.json +++ b/its/ruling/src/test/resources/expected/python-S116.json @@ -526,7 +526,6 @@ 'project:twisted-12.1.0/twisted/internet/task.py':[ 63, 64, -112, 124, 374, 375, @@ -573,7 +572,6 @@ 212, 212, 224, -763, ], 'project:twisted-12.1.0/twisted/internet/test/test_tls.py':[ 42, @@ -1230,8 +1228,6 @@ ], 'project:twisted-12.1.0/twisted/trial/test/test_tests.py':[ 536, -537, -537, 538, ], 'project:twisted-12.1.0/twisted/trial/test/test_util.py':[ diff --git a/python-checks/src/main/java/org/sonar/python/checks/FieldNameCheck.java b/python-checks/src/main/java/org/sonar/python/checks/FieldNameCheck.java index 15f3d0089..2a692f5b1 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/FieldNameCheck.java +++ b/python-checks/src/main/java/org/sonar/python/checks/FieldNameCheck.java @@ -19,22 +19,21 @@ */ package org.sonar.python.checks; -import com.sonar.sslr.api.AstNode; -import com.sonar.sslr.api.AstNodeType; -import com.sonar.sslr.api.Token; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.sonar.check.Rule; import org.sonar.check.RuleProperty; -import org.sonar.python.PythonCheckAstNode; -import org.sonar.python.api.PythonGrammar; +import org.sonar.python.PythonSubscriptionCheck; +import org.sonar.python.api.tree.PyClassDefTree; +import org.sonar.python.api.tree.Tree; +import org.sonar.python.semantic.TreeSymbol; +import org.sonar.python.semantic.Usage; -@Rule(key = FieldNameCheck.CHECK_KEY) -public class FieldNameCheck extends PythonCheckAstNode { - - public static final String CHECK_KEY = "S116"; +@Rule(key = "S116") +public class FieldNameCheck extends PythonSubscriptionCheck { private static final String MESSAGE = "Rename this field \"%s\" to match the regular expression %s."; @@ -44,41 +43,34 @@ public class FieldNameCheck extends PythonCheckAstNode { @RuleProperty(key = "format", defaultValue = DEFAULT) public String format = DEFAULT; - private Pattern pattern = null; - private Pattern constantPattern = null; - - @Override - public Set subscribedKinds() { - return Collections.singleton(PythonGrammar.CLASSDEF); - } @Override - public void visitNode(AstNode astNode) { - if (!CheckUtils.classHasInheritance(astNode)) { - List allFields = new NewSymbolsAnalyzer().getClassFields(astNode); - checkNames(allFields); - } - } - - private void checkNames(List varNames) { - if (constantPattern == null) { - constantPattern = Pattern.compile(CONSTANT_PATTERN); - } - for (Token name : varNames) { - if (!constantPattern.matcher(name.getValue()).matches()) { - checkName(name); + public void initialize(Context context) { + Pattern pattern = Pattern.compile(format); + Pattern constantPattern = Pattern.compile(CONSTANT_PATTERN); + context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, ctx -> { + PyClassDefTree classDef = (PyClassDefTree) ctx.syntaxNode(); + if (CheckUtils.classHasInheritance(classDef)) { + return; + } + for (TreeSymbol field : fieldsToCheck(classDef)) { + String name = field.name(); + if (!pattern.matcher(name).matches() && !constantPattern.matcher(name).matches()) { + String message = String.format(MESSAGE, name, this.format); + field.usages().stream() + .filter(usage -> usage.kind() == Usage.Kind.ASSIGNMENT_LHS) + .limit(1) + .forEach(usage -> ctx.addIssue(usage.tree(), message)); + } } - } + }); } - private void checkName(Token token) { - String name = token.getValue(); - if (pattern == null) { - pattern = Pattern.compile(format); - } - if (!pattern.matcher(name).matches()) { - addIssue(token, String.format(MESSAGE, name, format)); - } + private static List fieldsToCheck(PyClassDefTree classDef) { + Set classFieldNames = classDef.classFields().stream().map(TreeSymbol::name).collect(Collectors.toSet()); + List result = new ArrayList<>(classDef.classFields()); + classDef.instanceFields().stream().filter(f -> !classFieldNames.contains(f.name())).forEach(result::add); + return result; } } diff --git a/python-checks/src/test/resources/checks/fieldName.py b/python-checks/src/test/resources/checks/fieldName.py index e0a8d80d7..3dc910a68 100644 --- a/python-checks/src/test/resources/checks/fieldName.py +++ b/python-checks/src/test/resources/checks/fieldName.py @@ -2,6 +2,7 @@ class MyClass: myField = 4 # Noncompliant {{Rename this field "myField" to match the regular expression ^[_a-z][a-z0-9_]+$.}} # ^^^^^^^ + myField = 5 myField2: int = 4 # Noncompliant {{Rename this field "myField2" to match the regular expression ^[_a-z][a-z0-9_]+$.}} # ^^^^^^^^ my_field = 4 @@ -9,10 +10,14 @@ class MyClass: def __init__(self): localVar = 0 self.myField = 0 + self.myField = 0 self.my_field1 = 0 self.myField1 = 0 # Noncompliant # ^^^^^^^^ + def instance_field_usage(self): + print(self.newField) + def fun(self): self.myField.field = 1 self.newField = 0 # Noncompliant diff --git a/python-squid/src/main/java/org/sonar/python/api/tree/PyClassDefTree.java b/python-squid/src/main/java/org/sonar/python/api/tree/PyClassDefTree.java index aec55d6b6..cd2757ccd 100644 --- a/python-squid/src/main/java/org/sonar/python/api/tree/PyClassDefTree.java +++ b/python-squid/src/main/java/org/sonar/python/api/tree/PyClassDefTree.java @@ -20,7 +20,9 @@ package org.sonar.python.api.tree; import java.util.List; +import java.util.Set; import javax.annotation.CheckForNull; +import org.sonar.python.semantic.TreeSymbol; public interface PyClassDefTree extends PyStatementTree { @@ -48,4 +50,8 @@ public interface PyClassDefTree extends PyStatementTree { @CheckForNull PyToken docstring(); + + Set classFields(); + + Set instanceFields(); } diff --git a/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionDefTree.java b/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionDefTree.java index 8f23f3624..500f19859 100644 --- a/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionDefTree.java +++ b/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionDefTree.java @@ -47,8 +47,6 @@ public interface PyFunctionDefTree extends PyStatementTree, PyFunctionLikeTree { PyStatementListTree body(); - boolean isMethodDefinition(); - @CheckForNull PyToken docstring(); diff --git a/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionLikeTree.java b/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionLikeTree.java index 1d91f0ae7..2f97f1137 100644 --- a/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionLikeTree.java +++ b/python-squid/src/main/java/org/sonar/python/api/tree/PyFunctionLikeTree.java @@ -28,4 +28,6 @@ public interface PyFunctionLikeTree extends Tree { PyParameterListTree parameters(); Set localVariables(); + + boolean isMethodDefinition(); } diff --git a/python-squid/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java b/python-squid/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java index cc839dbb1..5c77e16d6 100644 --- a/python-squid/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java +++ b/python-squid/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java @@ -35,12 +35,14 @@ import org.sonar.python.api.tree.HasSymbol; import org.sonar.python.api.tree.PyAliasedNameTree; import org.sonar.python.api.tree.PyAnnotatedAssignmentTree; +import org.sonar.python.api.tree.PyAnyParameterTree; import org.sonar.python.api.tree.PyAssignmentStatementTree; import org.sonar.python.api.tree.PyClassDefTree; import org.sonar.python.api.tree.PyCompoundAssignmentStatementTree; import org.sonar.python.api.tree.PyComprehensionForTree; import org.sonar.python.api.tree.PyDecoratorTree; import org.sonar.python.api.tree.PyDottedNameTree; +import org.sonar.python.api.tree.PyExpressionTree; import org.sonar.python.api.tree.PyFileInputTree; import org.sonar.python.api.tree.PyForStatementTree; import org.sonar.python.api.tree.PyFunctionDefTree; @@ -52,11 +54,13 @@ import org.sonar.python.api.tree.PyNameTree; import org.sonar.python.api.tree.PyNonlocalStatementTree; import org.sonar.python.api.tree.PyParameterListTree; +import org.sonar.python.api.tree.PyParameterTree; import org.sonar.python.api.tree.PyQualifiedExpressionTree; import org.sonar.python.api.tree.PyTupleTree; import org.sonar.python.api.tree.Tree; import org.sonar.python.api.tree.Tree.Kind; import org.sonar.python.tree.BaseTreeVisitor; +import org.sonar.python.tree.PyClassDefTreeImpl; import org.sonar.python.tree.PyFunctionDefTreeImpl; import org.sonar.python.tree.PyLambdaExpressionTreeImpl; import org.sonar.python.tree.PyNameTreeImpl; @@ -65,6 +69,7 @@ public class SymbolTableBuilder extends BaseTreeVisitor { private Map scopesByRootTree; + private Set assignmentLeftHandSides = new HashSet<>(); @Override public void visitFileInput(PyFileInputTree pyFileInputTree) { @@ -83,6 +88,13 @@ public void visitFileInput(PyFileInputTree pyFileInputTree) { } } }); + scopesByRootTree.values().stream() + .filter(scope -> scope.rootTree.is(Kind.CLASSDEF)) + .forEach(scope -> { + PyClassDefTreeImpl classDef = (PyClassDefTreeImpl) scope.rootTree; + scope.symbols.forEach(classDef::addClassField); + scope.instanceAttributesByName.values().forEach(classDef::addInstanceField); + }); } private static class ScopeVisitor extends BaseTreeVisitor { @@ -115,7 +127,7 @@ public void visitFileInput(PyFileInputTree tree) { public void visitLambda(PyLambdaExpressionTree pyLambdaExpressionTree) { createScope(pyLambdaExpressionTree, currentScope()); enterScope(pyLambdaExpressionTree); - createParameters(pyLambdaExpressionTree.parameters()); + createParameters(pyLambdaExpressionTree); super.visitLambda(pyLambdaExpressionTree); leaveScope(); } @@ -124,7 +136,7 @@ public void visitLambda(PyLambdaExpressionTree pyLambdaExpressionTree) { public void visitFunctionDef(PyFunctionDefTree pyFunctionDefTree) { createScope(pyFunctionDefTree, currentScope()); enterScope(pyFunctionDefTree); - createParameters(pyFunctionDefTree.parameters()); + createParameters(pyFunctionDefTree); super.visitFunctionDef(pyFunctionDefTree); leaveScope(); } @@ -192,11 +204,25 @@ private void createLoopVariables(PyForStatementTree loopTree) { }); } - private void createParameters(@Nullable PyParameterListTree parameterList) { - if (parameterList == null) { + private void createParameters(PyFunctionLikeTree function) { + PyParameterListTree parameterList = function.parameters(); + if (parameterList == null || parameterList.all().isEmpty()) { return; } - parameterList.nonTuple().forEach(param -> addBindingUsage(param.name(), Usage.Kind.PARAMETER)); + + boolean hasSelf = false; + if (function.isMethodDefinition()) { + PyAnyParameterTree first = parameterList.all().get(0); + if (first.is(Kind.PARAMETER)) { + currentScope().createSelfParameter((PyParameterTree) first); + hasSelf = true; + } + } + + parameterList.nonTuple().stream() + .skip(hasSelf ? 1 : 0) + .forEach(param -> addBindingUsage(param.name(), Usage.Kind.PARAMETER)); + parameterList.all().stream() .filter(param -> param.is(Kind.TUPLE)) .flatMap(param -> ((PyTupleTree) param).elements().stream()) @@ -207,15 +233,30 @@ private void createParameters(@Nullable PyParameterListTree parameterList) { @Override public void visitAssignmentStatement(PyAssignmentStatementTree pyAssignmentStatementTree) { - pyAssignmentStatementTree.lhsExpressions().stream() + List lhs = pyAssignmentStatementTree.lhsExpressions().stream() .flatMap(exprList -> exprList.expressions().stream()) - .flatMap(expr -> expr.is(Kind.TUPLE) ? ((PyTupleTree) expr).elements().stream() : Stream.of(expr)) + .flatMap(this::flattenTuples) + .collect(Collectors.toList()); + + assignmentLeftHandSides.addAll(lhs); + + lhs.stream() .filter(expr -> expr.is(Kind.NAME)) .map(PyNameTree.class::cast) .forEach(name -> addBindingUsage(name, Usage.Kind.ASSIGNMENT_LHS)); + super.visitAssignmentStatement(pyAssignmentStatementTree); } + private Stream flattenTuples(PyExpressionTree expression) { + if (expression.is(Kind.TUPLE)) { + PyTupleTree tuple = (PyTupleTree) expression; + return tuple.elements().stream().flatMap(this::flattenTuples); + } else { + return Stream.of(expression); + } + } + @Override public void visitAnnotatedAssignment(PyAnnotatedAssignmentTree pyAnnotatedAssignmentTree) { if (pyAnnotatedAssignmentTree.variable().is(Kind.NAME)) { @@ -275,6 +316,7 @@ private static class Scope { private final Set symbols = new HashSet<>(); private final Set globalNames = new HashSet<>(); private final Set nonlocalNames = new HashSet<>(); + private final Map instanceAttributesByName = new HashMap<>(); private Scope(@Nullable Scope parent, Tree rootTree) { this.parent = parent; @@ -285,6 +327,14 @@ private Set symbols() { return Collections.unmodifiableSet(symbols); } + private void createSelfParameter(PyParameterTree parameter) { + PyNameTree nameTree = parameter.name(); + String symbolName = nameTree.name(); + SymbolImpl symbol = new SelfSymbolImpl(symbolName, parent); + symbols.add(symbol); + symbolsByName.put(symbolName, symbol); + symbol.addUsage(nameTree, Usage.Kind.PARAMETER); + } void addBindingUsage(PyNameTree nameTree, Usage.Kind kind, @Nullable String fullyQualifiedName) { String symbolName = nameTree.name(); @@ -357,7 +407,7 @@ void addUsage(Tree tree, Usage.Kind kind) { } } - void addOrCreateChildUsage(PyNameTree name) { + void addOrCreateChildUsage(PyNameTree name, Usage.Kind kind) { String childSymbolName = name.name(); if (!childrenSymbolByName.containsKey(childSymbolName)) { String childFullyQualifiedName = fullyQualifiedName != null @@ -367,7 +417,23 @@ void addOrCreateChildUsage(PyNameTree name) { childrenSymbolByName.put(childSymbolName, symbol); } TreeSymbol symbol = childrenSymbolByName.get(childSymbolName); - ((SymbolImpl) symbol).addUsage(name, Usage.Kind.OTHER); + ((SymbolImpl) symbol).addUsage(name, kind); + } + } + + private static class SelfSymbolImpl extends SymbolImpl { + + private final Scope classScope; + + SelfSymbolImpl(String name, Scope classScope) { + super(name, null); + this.classScope = classScope; + } + + @Override + void addOrCreateChildUsage(PyNameTree nameTree, Usage.Kind kind) { + SymbolImpl symbol = classScope.instanceAttributesByName.computeIfAbsent(nameTree.name(), name -> new SymbolImpl(name, null)); + symbol.addUsage(nameTree, kind); } } @@ -412,7 +478,8 @@ public void visitQualifiedExpression(PyQualifiedExpressionTree qualifiedExpressi if (qualifiedExpression.qualifier() instanceof HasSymbol) { TreeSymbol qualifierSymbol = ((HasSymbol) qualifiedExpression.qualifier()).symbol(); if (qualifierSymbol != null) { - ((SymbolImpl) qualifierSymbol).addOrCreateChildUsage(qualifiedExpression.name()); + Usage.Kind usageKind = assignmentLeftHandSides.contains(qualifiedExpression) ? Usage.Kind.ASSIGNMENT_LHS : Usage.Kind.OTHER; + ((SymbolImpl) qualifierSymbol).addOrCreateChildUsage(qualifiedExpression.name(), usageKind); } } } diff --git a/python-squid/src/main/java/org/sonar/python/tree/PyClassDefTreeImpl.java b/python-squid/src/main/java/org/sonar/python/tree/PyClassDefTreeImpl.java index 205b061bd..71f5d94e7 100644 --- a/python-squid/src/main/java/org/sonar/python/tree/PyClassDefTreeImpl.java +++ b/python-squid/src/main/java/org/sonar/python/tree/PyClassDefTreeImpl.java @@ -21,8 +21,10 @@ import com.sonar.sslr.api.AstNode; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.CheckForNull; @@ -35,6 +37,7 @@ import org.sonar.python.api.tree.PyToken; import org.sonar.python.api.tree.PyTreeVisitor; import org.sonar.python.api.tree.Tree; +import org.sonar.python.semantic.TreeSymbol; public class PyClassDefTreeImpl extends PyTree implements PyClassDefTree { @@ -47,6 +50,8 @@ public class PyClassDefTreeImpl extends PyTree implements PyClassDefTree { private final PyToken colon; private final PyStatementListTree body; private final PyToken docstring; + private final Set classFields = new HashSet<>(); + private final Set instanceFields = new HashSet<>(); public PyClassDefTreeImpl(AstNode astNode, List decorators, PyToken classKeyword, PyNameTree name, @Nullable PyToken leftPar, @Nullable PyArgListTree args, @Nullable PyToken rightPar, @@ -122,6 +127,24 @@ public PyToken docstring() { return docstring; } + @Override + public Set classFields() { + return classFields; + } + + @Override + public Set instanceFields() { + return instanceFields; + } + + public void addClassField(TreeSymbol field) { + classFields.add(field); + } + + public void addInstanceField(TreeSymbol field) { + instanceFields.add(field); + } + @Override public List children() { return Stream.of(decorators, Arrays.asList(classKeyword, name, leftPar, args, rightPar, colon, body, docstring)) diff --git a/python-squid/src/main/java/org/sonar/python/tree/PyLambdaExpressionTreeImpl.java b/python-squid/src/main/java/org/sonar/python/tree/PyLambdaExpressionTreeImpl.java index 145a80357..fd2f11ca9 100644 --- a/python-squid/src/main/java/org/sonar/python/tree/PyLambdaExpressionTreeImpl.java +++ b/python-squid/src/main/java/org/sonar/python/tree/PyLambdaExpressionTreeImpl.java @@ -76,6 +76,11 @@ public Set localVariables() { return symbols; } + @Override + public boolean isMethodDefinition() { + return false; + } + public void addLocalVariableSymbol(TreeSymbol symbol) { symbols.add(symbol); } diff --git a/python-squid/src/test/java/org/sonar/python/parser/PythonTestUtils.java b/python-squid/src/test/java/org/sonar/python/PythonTestUtils.java similarity index 55% rename from python-squid/src/test/java/org/sonar/python/parser/PythonTestUtils.java rename to python-squid/src/test/java/org/sonar/python/PythonTestUtils.java index bfd42285f..9d56473f1 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/PythonTestUtils.java +++ b/python-squid/src/test/java/org/sonar/python/PythonTestUtils.java @@ -17,15 +17,34 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.python.parser; +package org.sonar.python; + +import com.sonar.sslr.api.Grammar; +import com.sonar.sslr.impl.Parser; +import java.nio.charset.StandardCharsets; +import org.sonar.python.api.tree.PyFileInputTree; +import org.sonar.python.parser.PythonParser; +import org.sonar.python.semantic.SymbolTableBuilder; +import org.sonar.python.tree.PythonTreeMaker; public final class PythonTestUtils { + private static final Parser p = PythonParser.create(new PythonConfiguration(StandardCharsets.UTF_8)); + private static final PythonTreeMaker pythonTreeMaker = new PythonTreeMaker(); + + private PythonTestUtils() { + } + public static String appendNewLine(String s) { return s + "\n"; } - private PythonTestUtils() { + public static PyFileInputTree parse(String... lines) { + String code = String.join(System.getProperty("line.separator"), lines); + PyFileInputTree tree = pythonTreeMaker.fileInput(p.parse(code)); + new SymbolTableBuilder().visitFileInput(tree); + return tree; } + } diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ClassDefTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ClassDefTest.java index 2cf305582..e74b5f2a1 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ClassDefTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ClassDefTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/DecoratorTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/DecoratorTest.java index 38f097191..9de73ddf6 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/DecoratorTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/DecoratorTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ForStatementTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ForStatementTest.java index 4b5d84ba3..027e8b28c 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ForStatementTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/ForStatementTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/FuncDefTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/FuncDefTest.java index 7fae6920b..6706cb6b0 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/FuncDefTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/FuncDefTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/SuiteTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/SuiteTest.java index 6b0a25539..0142680db 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/SuiteTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/SuiteTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/WithStatementTest.java b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/WithStatementTest.java index 5ecca710b..14a0292d4 100644 --- a/python-squid/src/test/java/org/sonar/python/parser/compound/statements/WithStatementTest.java +++ b/python-squid/src/test/java/org/sonar/python/parser/compound/statements/WithStatementTest.java @@ -22,7 +22,7 @@ import org.junit.Before; import org.junit.Test; import org.sonar.python.api.PythonGrammar; -import org.sonar.python.parser.PythonTestUtils; +import org.sonar.python.PythonTestUtils; import org.sonar.python.parser.RuleTest; import static org.sonar.sslr.tests.Assertions.assertThat; diff --git a/python-squid/src/test/java/org/sonar/python/semantic/ClassSymbolTest.java b/python-squid/src/test/java/org/sonar/python/semantic/ClassSymbolTest.java new file mode 100644 index 000000000..dfdc32a69 --- /dev/null +++ b/python-squid/src/test/java/org/sonar/python/semantic/ClassSymbolTest.java @@ -0,0 +1,96 @@ +/* + * SonarQube Python Plugin + * Copyright (C) 2011-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.python.semantic; + +import org.junit.Test; +import org.sonar.python.api.tree.PyClassDefTree; +import org.sonar.python.api.tree.PyFileInputTree; +import org.sonar.python.api.tree.Tree; +import org.sonar.python.PythonTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + + +public class ClassSymbolTest { + @Test + public void no_field() { + PyClassDefTree empty = parseClass( + "class C: ", + " pass"); + assertThat(empty.classFields()).isEmpty(); + assertThat(empty.instanceFields()).isEmpty(); + + PyClassDefTree empty2 = parseClass( + "class C:", + " def f(): pass"); + assertThat(empty2.classFields()).isEmpty(); + assertThat(empty2.instanceFields()).isEmpty(); + } + + @Test + public void class_fields() { + PyClassDefTree c = parseClass( + "class C: ", + " f1 = 1", + " f1 = 2", + " f2 = 3"); + assertThat(c.classFields()).extracting(TreeSymbol::name).containsExactlyInAnyOrder("f1", "f2"); + assertThat(c.instanceFields()).isEmpty(); + } + + @Test + public void instance_fields() { + PyClassDefTree c1 = parseClass( + "class C: ", + " def f(self):", + " self.a = 1", + " self.b = 2", + " x = 2", + " def g(self):", + " self.a = 3", + " self.c = 4"); + assertThat(c1.classFields()).isEmpty(); + assertThat(c1.instanceFields()).extracting(TreeSymbol::name).containsExactlyInAnyOrder("a", "b", "c"); + + PyClassDefTree c2 = parseClass( + "class C:", + " def f(self):", + " print(self.a)", + " def g(self):", + " self.a = 1"); + assertThat(c2.classFields()).isEmpty(); + assertThat(c2.instanceFields()).extracting(TreeSymbol::name).containsExactlyInAnyOrder("a"); + TreeSymbol field = c2.instanceFields().iterator().next(); + assertThat(field.usages()) + .extracting(Usage::kind, u -> u.tree().firstToken().line()) + .containsExactlyInAnyOrder( + tuple(Usage.Kind.OTHER, 3), + tuple(Usage.Kind.ASSIGNMENT_LHS, 5)); + } + + private PyClassDefTree parseClass(String... lines) { + PyFileInputTree fileInput = PythonTestUtils.parse(lines); + return fileInput.descendants(Tree.Kind.CLASSDEF) + .map(PyClassDefTree.class::cast) + .findFirst().get(); + } + +} diff --git a/python-squid/src/test/java/org/sonar/python/semantic/FullyQualifiedNameTest.java b/python-squid/src/test/java/org/sonar/python/semantic/FullyQualifiedNameTest.java index b3c404fc7..e20e2beaa 100644 --- a/python-squid/src/test/java/org/sonar/python/semantic/FullyQualifiedNameTest.java +++ b/python-squid/src/test/java/org/sonar/python/semantic/FullyQualifiedNameTest.java @@ -19,25 +19,19 @@ */ package org.sonar.python.semantic; -import com.sonar.sslr.api.Grammar; -import com.sonar.sslr.impl.Parser; -import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import org.junit.Test; -import org.sonar.python.PythonConfiguration; import org.sonar.python.api.tree.PyCallExpressionTree; import org.sonar.python.api.tree.PyFileInputTree; import org.sonar.python.api.tree.PyNameTree; import org.sonar.python.api.tree.PyQualifiedExpressionTree; import org.sonar.python.api.tree.Tree; -import org.sonar.python.parser.PythonParser; -import org.sonar.python.tree.PythonTreeMaker; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.python.PythonTestUtils.parse; public class FullyQualifiedNameTest { - private Parser p = PythonParser.create(new PythonConfiguration(StandardCharsets.UTF_8)); - private PythonTreeMaker pythonTreeMaker = new PythonTreeMaker(); + @Test public void callee_qualified_expression() { PyFileInputTree tree = parse( @@ -282,11 +276,4 @@ private void assertNameAndQualifiedName(PyFileInputTree tree, String name, @Null assertThat(symbol.fullyQualifiedName()).isEqualTo(qualifiedName); } - private PyFileInputTree parse(String ...lines) { - String code = String.join(System.getProperty("line.separator"), lines); - PyFileInputTree tree = pythonTreeMaker.fileInput(p.parse(code)); - new SymbolTableBuilder().visitFileInput(tree); - return tree; - } - }