Skip to content

Commit

Permalink
SONARPY-1486 Support generic type alias declaration syntax (#1610)
Browse files Browse the repository at this point in the history
  • Loading branch information
maksim-grebeniuk-sonarsource authored Oct 20, 2023
1 parent 8f382be commit b8423a1
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ def builtins(x: int, y: str):
if 42 == y: ... # Noncompliant
if x == None: ... # OK
if None == y: ... # OK

type T = str
def foo(a: T):
if a == 42: # FN
...
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,14 @@ def match_statement_no_fp_reassignment(value):
match value:
case x: # OK, though should be raised by S1854 (dead store)
x = 42

# To be fixed in https://sonarsource.atlassian.net/browse/SONARPY-1524 ticket
def type_aliases_statement_fp_reference():
type A = B # Noncompliant
type B = getType() # Noncompliant

def getType():
return str

class C[A]():
...
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,14 @@ class ClassInFunction:

def nested_function() -> ClassInFunction:
pass


def type_aliases():
type A = B
type B = getType()

def getType():
return str

class C[A]():
...
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ enum Kind {
WITH_INSTANCE,
GLOBAL_DECLARATION,
PATTERN_DECLARATION,
TYPE_PARAM_DECLARATION
TYPE_PARAM_DECLARATION,
TYPE_ALIAS_DECLARATION,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,11 @@ public void visitTypeParam(TypeParam typeParam) {
scan(typeParam.name());
scan(typeParam.typeAnnotation());
}

@Override
public void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement) {
scan(typeAliasStatement.name());
scan(typeAliasStatement.typeParams());
scan(typeAliasStatement.expression());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ enum Kind {

YIELD_STMT(YieldStatement.class),

TYPE_ALIAS_STMT(TypeAliasStatement.class),

PARENTHESIZED(ParenthesizedExpression.class),

UNPACKING_EXPR(UnpackingExpression.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,6 @@ public interface TreeVisitor {
void visitTypeParams(TypeParams typeParams);

void visitTypeParam(TypeParam typeParam);

void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2023 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.plugins.python.api.tree;

import javax.annotation.CheckForNull;

/**
* <pre>
* type {@link #name()} {@link #typeParams()} = {@link #expression()}
* </pre>
*
* See https://docs.python.org/3/reference/simple_stmts.html#the-type-statement
*/
public interface TypeAliasStatement extends Statement {
Token typeKeyword();

Name name();

@CheckForNull
TypeParams typeParams();

Token equalToken();

Expression expression();

}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public enum PythonGrammar implements GrammarRuleKey {
FUN_RETURN_ANNOTATION,
TYPE_PARAMS,
TYPE_PARAM,
TYPE_ALIAS_STMT,

CLASSDEF,
CLASSNAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ protected void expressions(LexerfulGrammarBuilder b) {
protected void simpleStatements(LexerfulGrammarBuilder b) {
simpleStatement(b);

b.rule(TYPE_ALIAS_STMT).is("type", NAME, b.optional(TYPE_PARAMS), "=", TEST);
b.rule(PRINT_STMT).is("print", b.nextNot("="), b.nextNot("("), b.firstOf(
b.sequence(">>", TEST, b.optional(b.oneOrMore(",", TEST), b.optional(","))),
b.optional(TEST, b.zeroOrMore(",", TEST), b.optional(","))));
Expand Down Expand Up @@ -269,6 +270,7 @@ protected void simpleStatements(LexerfulGrammarBuilder b) {

protected void simpleStatement(LexerfulGrammarBuilder b) {
b.rule(SIMPLE_STMT).is(b.firstOf(
TYPE_ALIAS_STMT,
PRINT_STMT,
EXEC_STMT,
EXPRESSION_STMT,
Expand All @@ -282,7 +284,8 @@ protected void simpleStatement(LexerfulGrammarBuilder b) {
CONTINUE_STMT,
IMPORT_STMT,
GLOBAL_STMT,
NONLOCAL_STMT));
NONLOCAL_STMT
));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.plugins.python.api.tree.TupleParameter;
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
import org.sonar.plugins.python.api.tree.TypeAnnotation;
import org.sonar.plugins.python.api.tree.TypeParams;
import org.sonar.plugins.python.api.tree.WithItem;
Expand Down Expand Up @@ -342,6 +343,12 @@ private void createTypeParameters(@Nullable TypeParams typeParams) {
.forEach(typeParam -> addBindingUsage(typeParam.name(), Usage.Kind.TYPE_PARAM_DECLARATION));
}

@Override
public void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement) {
addBindingUsage(typeAliasStatement.name(), Usage.Kind.TYPE_ALIAS_DECLARATION);
super.visitTypeAliasStatement(typeAliasStatement);
}

@Override
public void visitClassDef(ClassDef pyClassDefTree) {
String className = pyClassDefTree.name().name();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.TryStatement;
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
import org.sonar.plugins.python.api.tree.TypeAnnotation;
import org.sonar.plugins.python.api.tree.TypeParam;
import org.sonar.plugins.python.api.tree.TypeParams;
Expand Down Expand Up @@ -236,9 +237,23 @@ protected Statement statement(StatementWithSeparator statementWithSeparator) {
if (astNode.is(PythonGrammar.MATCH_STMT)) {
return matchStatement(astNode);
}
if (astNode.is(PythonGrammar.TYPE_ALIAS_STMT)) {
return typeAliasStatement(statementWithSeparator);
}
throw new IllegalStateException("Statement " + astNode.getType() + " not correctly translated to strongly typed AST");
}

public TypeAliasStatement typeAliasStatement(StatementWithSeparator statementWithSeparator) {
var astNode = statementWithSeparator.statement();
var separator = statementWithSeparator.separator();
var typeDef = toPyToken(astNode.getChildren().get(0).getToken());
var name = name(astNode.getFirstChild(PythonGrammar.NAME));
var typeParams = typeParams(astNode);
var equalToken = toPyToken(astNode.getFirstChild(PythonPunctuator.ASSIGN).getToken());
var expression = expression(astNode.getFirstChild(PythonGrammar.TEST));
return new TypeAliasStatementImpl(typeDef, name, typeParams, equalToken, expression, separator);
}

public AnnotatedAssignment annotatedAssignment(StatementWithSeparator statementWithSeparator) {
AstNode astNode = statementWithSeparator.statement();
Separators separators = statementWithSeparator.separator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2023 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.tree;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.TreeVisitor;
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
import org.sonar.plugins.python.api.tree.TypeParams;

public class TypeAliasStatementImpl extends SimpleStatement implements TypeAliasStatement {
private final Token typeKeyword;
private final Name name;
private final TypeParams typeParams;
private final Token equalToken;
private final Expression expression;
private final Separators separator;

public TypeAliasStatementImpl(Token typeKeyword, Name name, @Nullable TypeParams typeParams,
Token equalToken, Expression expression, Separators separator) {
this.typeKeyword = typeKeyword;
this.name = name;
this.typeParams = typeParams;
this.equalToken = equalToken;
this.expression = expression;
this.separator = separator;
}

@Override
public void accept(TreeVisitor visitor) {
visitor.visitTypeAliasStatement(this);
}

@Override
public Kind getKind() {
return Kind.TYPE_ALIAS_STMT;
}

@Override
public Token typeKeyword() {
return typeKeyword;
}

@Override
public Name name() {
return name;
}

@CheckForNull
@Override
public TypeParams typeParams() {
return typeParams;
}

@Override
public Token equalToken() {
return equalToken;
}

@Override
public Expression expression() {
return expression;
}

@Override
List<Tree> computeChildren() {
var builder = Stream.<Tree>builder()
.add(typeKeyword())
.add(name())
.add(typeParams())
.add(equalToken()).add(expression());

separator.elements().forEach(builder::add);
return builder.build().filter(Objects::nonNull).collect(Collectors.toList());
}

@Override
public Token separator() {
return separator.last();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,19 @@ void class_pattern() {
verify(visitor).visitName(keywordPattern.attributeName());
verify(visitor).visitLiteralPattern(((LiteralPattern) keywordPattern.pattern()));
}

@Test
void type_alias_statement() {
setRootRule(PythonGrammar.TYPE_ALIAS_STMT);
var node = p.parse("type A[B] = str");
var statementWithSeparator = new StatementWithSeparator(node, null);
var typeAliasStatement = treeMaker.typeAliasStatement(statementWithSeparator);

var visitor = spy(FirstLastTokenVerifierVisitor.class);
typeAliasStatement.accept(visitor);

verify(visitor).visitTypeAliasStatement(typeAliasStatement);
verify(visitor).visitName(typeAliasStatement.name());
verify(visitor).visitName((Name) typeAliasStatement.expression());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ void global_variable() {
"function_with_lambdas", "var_with_usages_in_decorator", "fn_inside_comprehension_same_name", "with_instance", "exception_instance", "unpacking",
"using_builtin_symbol", "keyword_usage", "comprehension_vars", "parameter_default_value", "assignment_expression", "importing_stdlib", "importing_submodule",
"importing_submodule_as", "importing_submodule_after_parent", "importing_submodule_after_parent_nested", "importing_parent_after_submodule",
"importing_parent_after_submodule_2", "importing_submodule_twice", "importing_unknown_submodule", "type_params");
"importing_parent_after_submodule_2", "importing_submodule_twice", "importing_unknown_submodule", "type_params", "type_alias");

List<String> globalSymbols = new ArrayList<>(topLevelFunctions);
globalSymbols.addAll(Arrays.asList("a", "global_x", "global_var"));
Expand Down Expand Up @@ -616,6 +616,15 @@ void type_params() {
assertThat(symbolByName.get("T").usages().get(0).kind()).isEqualTo(Usage.Kind.TYPE_PARAM_DECLARATION);
}

@Test
void type_alias_declaration() {
FunctionDef functionDef = functionTreesByName.get("type_alias");
Map<String, Symbol> symbolByName = getSymbolByName(functionDef);
assertThat(symbolByName).hasSize(1).containsKey("M");
assertThat(symbolByName.get("M").usages()).hasSize(2);
assertThat(symbolByName.get("M").usages().get(0).kind()).isEqualTo(Usage.Kind.TYPE_ALIAS_DECLARATION);
}

private static class TestVisitor extends BaseTreeVisitor {
@Override
public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2809,6 +2809,26 @@ void except_group_multiple() {
assertThat(((Name) exceptClause2.exception()).name()).isEqualTo("ValueError");
}

@Test
void typeAliasStatement() {
setRootRule(PythonGrammar.TYPE_ALIAS_STMT);

var astNode = p.parse("type A[B] = str");
var statementWithSeparator = new StatementWithSeparator(astNode, null);
var typeAlias = treeMaker.typeAliasStatement(statementWithSeparator);

assertThat(typeAlias).isNotNull();
assertThat(typeAlias.typeKeyword()).isNotNull();
assertThat(typeAlias.typeKeyword().value()).isEqualTo("type");
assertThat(typeAlias.name()).isNotNull();
assertThat(typeAlias.name().name()).isEqualTo("A");
assertThat(typeAlias.typeParams()).isNotNull();
assertThat(typeAlias.equalToken()).isNotNull();
assertThat(typeAlias.expression()).isNotNull();
assertThat(typeAlias.expression().is(Kind.NAME)).isTrue();
assertThat(((Name) typeAlias.expression()).name()).isEqualTo("str");
}

public String fileContent(File file) {
try {
return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
Expand Down
4 changes: 4 additions & 0 deletions python-frontend/src/test/resources/semantic/symbols2.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,7 @@ def importing_unknown_submodule():
def type_params[T: str]():
a : T = "abc"
return a

def type_alias():
type M = str
return list[M]
1 change: 1 addition & 0 deletions python-frontend/src/test/resources/separator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
while i < 10:
i+=1
pass
type a = str

0 comments on commit b8423a1

Please sign in to comment.