Skip to content

Commit

Permalink
SONARPY-1511 Rule S6794: Type aliases should be declared with a type …
Browse files Browse the repository at this point in the history
…statement. (#1617)
  • Loading branch information
Jeremi Do Dinh authored Oct 24, 2023
1 parent 2af014f commit 60f735a
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ public static Iterable<Class> getChecks() {
TooManyReturnsCheck.class,
TrailingCommentCheck.class,
TrailingWhitespaceCheck.class,
TypeAliasAnnotationCheck.class,
ReferencedBeforeAssignmentCheck.class,
RegexComplexityCheck.class,
RegexLookaheadCheck.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.checks;

import java.util.Optional;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.PythonVersionUtils;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.TypeAnnotation;

@Rule(key = "S6794")
public class TypeAliasAnnotationCheck extends PythonSubscriptionCheck {

private static final String MESSAGE = "Use a \"type\" statement instead of this \"TypeAlias\".";

@Override
public void initialize(Context context) {
context.registerSyntaxNodeConsumer(Tree.Kind.VARIABLE_TYPE_ANNOTATION, TypeAliasAnnotationCheck::checkTypeAliasVariableAnnotation);
}

public static void checkTypeAliasVariableAnnotation(SubscriptionContext ctx) {
if (!supportsTypeParameterSyntax(ctx)) {
return;
}
TypeAnnotation typeAnnotation = (TypeAnnotation) ctx.syntaxNode();
Optional.of(typeAnnotation.expression())
.filter(HasSymbol.class::isInstance)
.map(HasSymbol.class::cast)
.map(HasSymbol::symbol)
.map(Symbol::fullyQualifiedName)
.filter("typing.TypeAlias"::equals)
.ifPresent(fqn -> ctx.addIssue(typeAnnotation.parent(), MESSAGE));
}

private static boolean supportsTypeParameterSyntax(SubscriptionContext ctx) {
PythonVersionUtils.Version required = PythonVersionUtils.Version.V_312;

// All versions must be greater than or equal to the required version.
return ctx.sourcePythonVersions().stream()
.allMatch(version -> version.compare(required.major(), required.minor()) >= 0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<p>This rule raises an issue when a type alias is declared outside of a <code>type</code> statement.</p>
<h2>Why is this an issue?</h2>
<p>Since Python 3.12 the keyword <code>type</code> is used to defined type aliases. It replaces the following construct:</p>
<pre>
from typing import TypeAlias, TypeVar

_T = TypeVar("_T")

MyTypeAlias: TypeAlias = set[_T]
</pre>
<p>Using the <code>type</code> statement to define type aliases allows for a more concise code and thus better readability. This also makes it
possible to declutter the code, as imports from the <code>typing</code> module (<code>TypeAlias</code> and <code>TyperVar</code>) can be removed.</p>
<pre>
type MyTypeAlias[T] = set[T]
</pre>
<h3>Exceptions</h3>
<p>This rule will only raise an issue when the Python version of the analyzed project is set to 3.12 or higher.</p>
<h2>How to fix it</h2>
<p>Use a <code>type</code> statement to declare the <code>TypeAlias</code> instead of using a regular assignment.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
from typing import TypeAlias

MyStringAlias: TypeAlias = str # Noncompliant: this TypeAlias can be more concise with the help of the type statement.

_T = TypeVar("_T")
MyGenericAlias: TypeAlias = list[_T] # Noncompliant: the type statement can help replace both the TypeVar and the TypeAlias statements.
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
type MyStringAlias = str # Compliant

type MyGenericAlias[T] = list[T] # Compliant
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li> Python Documentation - <a href="https://docs.python.org/3.12/reference/simple_stmts.html#type">The type statement</a> </li>
<li> Python 3.12 Release Notes - <a href="https://docs.python.org/3.12/whatsnew/3.12.html#pep-695-type-parameter-syntax">PEP 695: Type Parameter
Syntax</a> </li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"title": "Type aliases should be declared with a \"type\" statement",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-6794",
"sqKey": "S6794",
"scope": "All",
"quickfix": "unknown",
"code": {
"impacts": {
"MAINTAINABILITY": "MEDIUM"
},
"attribute": "CONVENTIONAL"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
"S6741",
"S6742",
"S6792",
"S6794",
"S6796"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.checks;

import java.util.EnumSet;
import org.junit.jupiter.api.Test;
import org.sonar.plugins.python.api.ProjectPythonVersion;
import org.sonar.plugins.python.api.PythonVersionUtils;
import org.sonar.python.checks.utils.PythonCheckVerifier;

import static org.assertj.core.api.Assertions.assertThat;

class TypeAliasAnnotationCheckTest {

@Test
void verify_python_312_issues() {
ProjectPythonVersion.setCurrentVersions(EnumSet.of(PythonVersionUtils.Version.V_312));
PythonCheckVerifier.verify("src/test/resources/checks/typeAliasAnnotation.py", new TypeAliasAnnotationCheck());
}

@Test
void verify_earlier_version_no_issues() {
ProjectPythonVersion.setCurrentVersions(EnumSet.of(PythonVersionUtils.Version.V_311, PythonVersionUtils.Version.V_312));
var issues = PythonCheckVerifier.issues("src/test/resources/checks/typeAliasAnnotation.py", new TypeAliasAnnotationCheck());
assertThat(issues)
.isEmpty();
}

}
27 changes: 27 additions & 0 deletions python-checks/src/test/resources/checks/typeAliasAnnotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
def scope_1():
from typing import TypeAlias, TypeVar

_T = TypeVar("_T")

BadTypeAlias: TypeAlias = set[_T] # Noncompliant {{Use a "type" statement instead of this "TypeAlias".}}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

type GoodTypeAlias[T] = set[T] # OK

def scope_2():
import typing as tp

_T = tp.TypeVar("_T")

BadTypeAlias: tp.TypeAlias = set[_T] # Noncompliant {{Use a "type" statement instead of this "TypeAlias".}}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

def scope_3():
from typing import TypeVar
_T = TypeVar("_T")
class TypeAlias:
...

BadTypeAlias: TypeAlias = set[_T]


0 comments on commit 60f735a

Please sign in to comment.