Skip to content

Commit 77ec919

Browse files
alban-auzeillpynicolas
authored andcommitted
SONARPY-247 Support variable annotations (SonarSource#128)
* SONARPY-247 Parse variable annotations * SONARPY-247 Support variable annotations in symbol table
1 parent 9c445aa commit 77ec919

18 files changed

Lines changed: 218 additions & 24 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public static boolean classHasInheritance(AstNode classDef) {
8686
}
8787

8888
public static boolean isAssignmentExpression(AstNode expression) {
89+
if (expression.is(PythonGrammar.EXPRESSION_STMT)) {
90+
AstNode assignNode = expression.getFirstChild(PythonGrammar.ANNASSIGN);
91+
if (assignNode != null && assignNode.getFirstChild(PythonPunctuator.ASSIGN) != null) {
92+
return true;
93+
}
94+
}
8995
int numberOfChildren = expression.getNumberOfChildren();
9096
int numberOfAssign = expression.getChildren(PythonPunctuator.ASSIGN).size();
9197
if (numberOfChildren == 3 && numberOfAssign == 1) {

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedList;
2626
import java.util.List;
2727
import org.sonar.python.api.PythonGrammar;
28+
import org.sonar.python.api.PythonPunctuator;
2829

2930
public class NewSymbolsAnalyzer {
3031

@@ -60,11 +61,14 @@ private void addFieldsInMethod(AstNode method) {
6061
}
6162

6263
private static List<AstNode> getTestsFromLongAssignmentExpression(AstNode expression) {
63-
List<AstNode> assignedExpressions = expression.getChildren(PythonGrammar.TESTLIST_STAR_EXPR);
64-
assignedExpressions.remove(assignedExpressions.size() - 1);
6564
List<AstNode> tests = new LinkedList<>();
66-
for (AstNode assignedExpression : assignedExpressions) {
67-
tests.addAll(assignedExpression.getDescendants(PythonGrammar.TEST));
65+
AstNode lastAssign = expression.getLastChild(PythonPunctuator.ASSIGN);
66+
for (AstNode node : expression.getChildren()) {
67+
if (node.is(PythonGrammar.TESTLIST_STAR_EXPR)) {
68+
tests.addAll(node.getDescendants(PythonGrammar.TEST));
69+
} else if (node == lastAssign) {
70+
break;
71+
}
6872
}
6973
return tests;
7074
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,22 @@ public void visitNode(AstNode node) {
6666
addImportedName(node, importedName);
6767

6868
} else {
69-
for (AstNode assignOperator : node.getChildren(PythonPunctuator.ASSIGN)) {
70-
AstNode assigned = assignOperator.getPreviousSibling();
71-
if (CheckUtils.equalNodes(assigned, assignOperator.getNextSibling()) && !isException(node, assigned)) {
72-
addIssue(assignOperator, MESSAGE);
69+
checkExpressionStatement(node);
70+
}
71+
}
72+
73+
private void checkExpressionStatement(AstNode node) {
74+
for (AstNode assignOperator : node.getChildren(PythonPunctuator.ASSIGN, PythonGrammar.ANNASSIGN)) {
75+
AstNode assigned = assignOperator.getPreviousSibling();
76+
if (assignOperator.is(PythonGrammar.ANNASSIGN)) {
77+
assignOperator = assignOperator.getFirstChild(PythonPunctuator.ASSIGN);
78+
if (assigned.is(PythonGrammar.TESTLIST_STAR_EXPR) && assigned.getNumberOfChildren() == 1) {
79+
assigned = assigned.getFirstChild();
7380
}
7481
}
82+
if (assignOperator != null && CheckUtils.equalNodes(assigned, assignOperator.getNextSibling()) && !isException(node, assigned)) {
83+
addIssue(assignOperator, MESSAGE);
84+
}
7585
}
7686
}
7787

python-checks/src/test/java/org/sonar/python/checks/CheckUtilsTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,16 @@ public void invalid_string_literal_content() throws Exception {
146146
CheckUtils.stringLiteralContent(PARSER.parse("2").getTokens().get(0).getOriginalValue());
147147
}
148148

149+
@Test
150+
public void is_assignment_expression() throws Exception {
151+
Function<String, Boolean> firstStatement = (source) ->
152+
CheckUtils.isAssignmentExpression(PARSER.parse(source).getFirstDescendant(PythonGrammar.SIMPLE_STMT).getFirstChild());
153+
154+
assertThat(firstStatement.apply("a()")).isFalse();
155+
assertThat(firstStatement.apply("a = 2")).isTrue();
156+
assertThat(firstStatement.apply("a: int")).isFalse();
157+
assertThat(firstStatement.apply("a: int = 2")).isTrue();
158+
assertThat(firstStatement.apply("a.b = (1, 2)")).isTrue();
159+
assertThat(firstStatement.apply("a.b: int = (1, 2)")).isTrue();
160+
}
149161
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def hello(self):
2525
fOo = 1 # Noncompliant {{Rename field "fOo" to prevent any misunderstanding/clash with field "foo" defined on line 23}}
2626
# ^^^
2727

28+
foO: int = 1 # Noncompliant {{Rename field "foO" to prevent any misunderstanding/clash with field "fOo" defined on line 25}}
29+
# ^^^
30+
2831
baz = 100
2932

3033
bat = 1000
@@ -36,7 +39,9 @@ class B:
3639
bar = 10
3740

3841
def __init__(self):
39-
self.baR = 10 # Noncompliant {{Rename field "baR" to prevent any misunderstanding/clash with field "bar" defined on line 36}}
42+
self.baR = 10 # Noncompliant {{Rename field "baR" to prevent any misunderstanding/clash with field "bar" defined on line 39}}
43+
# ^^^
44+
self.FoO: int = 10 # Noncompliant
4045
# ^^^
4146
self.baz = 100
4247
self.baT = 1000

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class MyClass:
22
myClass = 3 # Noncompliant {{Rename field "myClass"}}
33
# ^^^^^^^
44
def __int__(self, myclass):
5-
self.myclass = myclass # Noncompliant [[secondary=-4]]
5+
self.myclass: type = myclass # Noncompliant [[secondary=-4]]
66

77
def fun(self):
88
self.myClass += 1
@@ -30,3 +30,6 @@ class MyClass:
3030
def foo(self):
3131
self.myCLASS = 1 # Noncompliant
3232
myClass = 1 # compliant, local var
33+
34+
class MyClass2:
35+
myClass2: int = 3 # Noncompliant {{Rename field "myClass2"}}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
class MyClass:
33
myField = 4 # Noncompliant {{Rename this field "myField" to match the regular expression ^[_a-z][a-z0-9_]+$.}}
44
# ^^^^^^^
5+
myField2: int = 4 # Noncompliant {{Rename this field "myField2" to match the regular expression ^[_a-z][a-z0-9_]+$.}}
6+
# ^^^^^^^^
57
my_field = 4
68

79
def __init__(self):

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
def fun(input_par1, inputPar2, inputPar3 = 3): # Noncompliant {{Rename this parameter "inputPar3" to match the regular expression ^[_a-z][a-z0-9_]+$.}}
33
someName = 1 # Noncompliant {{Rename this local variable "someName" to match the regular expression ^[_a-z][a-z0-9_]+$.}}
44
# ^^^^^^^^
5+
CamelName: int = 1 # Noncompliant {{Rename this local variable "CamelName" to match the regular expression ^[_a-z][a-z0-9_]+$.}}
6+
# ^^^^^^^^^
57
another_name = someName
68
someName = 3
79
inputPar2 = 2

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
x = 1
55
x = y
66
x = x # Noncompliant
7+
# ^
8+
x: int = x # Noncompliant
9+
# ^
710
x += x
811

912
def f():
@@ -23,6 +26,7 @@ def f():
2326

2427
a.x = a.x # Noncompliant
2528
a[x] = a[x] # Noncompliant
29+
a[x]: str = a[x] # Noncompliant
2630
a[sideEffect()] = a[sideEffect()]
2731

2832
import1 = import1

python-squid/src/main/java/org/sonar/python/api/PythonGrammar.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public enum PythonGrammar implements GrammarRuleKey {
100100
EXEC_STMT,
101101
ASSERT_STMT,
102102

103+
ANNASSIGN,
103104
AUGASSIGN,
104105

105106
PASS_STMT,
@@ -172,11 +173,17 @@ public static LexerfulGrammarBuilder create() {
172173

173174
public static void grammar(LexerfulGrammarBuilder b) {
174175

176+
// https://github.com/python/cpython/blob/v3.6.4/Grammar/Grammar#L41
175177
b.rule(EXPRESSION_STMT).is(
176178
TESTLIST_STAR_EXPR,
177179
b.firstOf(
180+
ANNASSIGN,
178181
b.sequence(AUGASSIGN, b.firstOf(YIELD_EXPR, TESTLIST)),
179182
b.zeroOrMore("=", b.firstOf(YIELD_EXPR, TESTLIST_STAR_EXPR))));
183+
184+
// https://github.com/python/cpython/blob/v3.6.4/Grammar/Grammar#L43
185+
b.rule(ANNASSIGN).is(":", TEST, b.optional( "=", TEST));
186+
180187
b.rule(TESTLIST_STAR_EXPR).is(b.firstOf(TEST, STAR_EXPR), b.zeroOrMore(",", b.firstOf(TEST, STAR_EXPR)), b.optional(","));
181188
b.rule(AUGASSIGN).is(b.firstOf("+=", "-=", "*=", "/=", "//=", "%=", "**=", ">>=", "<<=", "&=", "^=", "|=", "@="));
182189

0 commit comments

Comments
 (0)