PY-71549 Convert some syntax-related annotators to visitors and add them to PySyntaxAnnotator

GitOrigin-RevId: 46f4a0ccd8c2e7ef45ce5fa37ec5c48376924587
This commit is contained in:
Petr
2025-07-16 04:51:32 +00:00
committed by intellij-monorepo-bot
parent 09cc73185a
commit 2e6c983cb2
9 changed files with 381 additions and 433 deletions
@@ -170,14 +170,7 @@
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PySyntaxAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyDocStringAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyDefinitionsAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyGeneratorInArgumentListAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyStarAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyStringLiteralQuotesAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyFStringsAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyPatternAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyTryExceptAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyLocalVariableAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyTypeParameterListAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyCompositeAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyFunctionHighlightingAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyBuiltinAnnotator"/>
@@ -29,71 +29,63 @@ import org.jetbrains.annotations.NotNull;
/**
* @author Mikhail Golubev
*/
public final class PyFStringsAnnotator extends PyAnnotatorBase {
final class PyFStringsAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyFStringsAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void visitPyFStringFragment(@NotNull PyFStringFragment node) {
final PsiElement typeConversion = node.getTypeConversion();
if (typeConversion != null) {
final String conversionChar = typeConversion.getText().substring(1);
if (conversionChar.isEmpty()) {
report(typeConversion, PyPsiBundle.message("ANN.fstrings.missing.conversion.character"));
}
else if (conversionChar.length() > 1 || "sra".indexOf(conversionChar.charAt(0)) < 0) {
report(typeConversion, PyPsiBundle.message("ANN.fstrings.illegal.conversion.character", conversionChar));
}
public void visitPyFStringFragment(@NotNull PyFStringFragment node) {
final PsiElement typeConversion = node.getTypeConversion();
if (typeConversion != null) {
final String conversionChar = typeConversion.getText().substring(1);
if (conversionChar.isEmpty()) {
report(typeConversion, PyPsiBundle.message("ANN.fstrings.missing.conversion.character"));
}
else if (conversionChar.length() > 1 || "sra".indexOf(conversionChar.charAt(0)) < 0) {
report(typeConversion, PyPsiBundle.message("ANN.fstrings.illegal.conversion.character", conversionChar));
}
}
}
@Override
public void visitPyFormattedStringElement(@NotNull PyFormattedStringElement node) {
final String wholeNodeText = node.getText();
for (TextRange textRange : node.getLiteralPartRanges()) {
int i = textRange.getStartOffset();
while (i < textRange.getEndOffset()) {
final int nextOffset = skipNamedUnicodeEscape(wholeNodeText, i, textRange.getEndOffset());
if (i != nextOffset) {
i = nextOffset;
@Override
public void visitPyFormattedStringElement(@NotNull PyFormattedStringElement node) {
final String wholeNodeText = node.getText();
for (TextRange textRange : node.getLiteralPartRanges()) {
int i = textRange.getStartOffset();
while (i < textRange.getEndOffset()) {
final int nextOffset = skipNamedUnicodeEscape(wholeNodeText, i, textRange.getEndOffset());
if (i != nextOffset) {
i = nextOffset;
continue;
}
final char c = wholeNodeText.charAt(i);
if (c == '}') {
if (i + 1 < textRange.getEndOffset() && wholeNodeText.charAt(i + 1) == '}') {
i += 2;
continue;
}
final char c = wholeNodeText.charAt(i);
if (c == '}') {
if (i + 1 < textRange.getEndOffset() && wholeNodeText.charAt(i + 1) == '}') {
i += 2;
continue;
}
reportCharacter(node, i, PyPsiBundle.message("ANN.fstrings.single.right.brace.not.allowed.inside.fstrings"));
}
i++;
reportCharacter(node, i, PyPsiBundle.message("ANN.fstrings.single.right.brace.not.allowed.inside.fstrings"));
}
i++;
}
}
private static int skipNamedUnicodeEscape(@NotNull String nodeText, int offset, int endOffset) {
if (StringUtil.startsWith(nodeText, offset, "\\N{")) {
final int rightBraceOffset = nodeText.indexOf('}', offset + 3);
return rightBraceOffset < 0 ? endOffset : rightBraceOffset + 1;
}
return offset;
}
public void reportCharacter(@NotNull PsiElement element, int offset, @NotNull @InspectionMessage String message) {
final int nodeStartOffset = element.getTextRange().getStartOffset();
myHolder.newAnnotation(HighlightSeverity.ERROR, message).range(TextRange.from(offset, 1).shiftRight(nodeStartOffset)).create();
}
public void report(@NotNull PsiElement element, @NotNull @InspectionMessage String error) {
myHolder.newAnnotation(HighlightSeverity.ERROR, error).range(element).create();
}
}
}
private static int skipNamedUnicodeEscape(@NotNull String nodeText, int offset, int endOffset) {
if (StringUtil.startsWith(nodeText, offset, "\\N{")) {
final int rightBraceOffset = nodeText.indexOf('}', offset + 3);
return rightBraceOffset < 0 ? endOffset : rightBraceOffset + 1;
}
return offset;
}
public void reportCharacter(@NotNull PsiElement element, int offset, @NotNull @InspectionMessage String message) {
final int nodeStartOffset = element.getTextRange().getStartOffset();
myHolder.newAnnotation(HighlightSeverity.ERROR, message).range(TextRange.from(offset, 1).shiftRight(nodeStartOffset)).create();
}
public void report(@NotNull PsiElement element, @NotNull @InspectionMessage String error) {
myHolder.newAnnotation(HighlightSeverity.ERROR, error).range(element).create();
}
}
@@ -16,7 +16,6 @@
package com.jetbrains.python.validation;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.PyArgumentList;
@@ -26,26 +25,19 @@ import com.jetbrains.python.psi.PyGeneratorExpression;
import org.jetbrains.annotations.NotNull;
public final class PyGeneratorInArgumentListAnnotator extends PyAnnotatorBase {
final class PyGeneratorInArgumentListAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyGeneratorInArgumentListAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void visitPyArgumentList(@NotNull PyArgumentList node) {
if (node.getArguments().length > 1) {
for (PyExpression expression : node.getArguments()) {
if (expression instanceof PyGeneratorExpression) {
ASTNode firstChildNode = expression.getNode().getFirstChildNode();
if (firstChildNode.getElementType() != PyTokenTypes.LPAR) {
myHolder.markError(expression, PyPsiBundle.message("ANN.generator.expression.must.be.parenthesized.if.not.sole.argument"));
}
public void visitPyArgumentList(@NotNull PyArgumentList node) {
if (node.getArguments().length > 1) {
for (PyExpression expression : node.getArguments()) {
if (expression instanceof PyGeneratorExpression) {
ASTNode firstChildNode = expression.getNode().getFirstChildNode();
if (firstChildNode.getElementType() != PyTokenTypes.LPAR) {
myHolder.markError(expression, PyPsiBundle.message("ANN.generator.expression.must.be.parenthesized.if.not.sole.argument"));
}
}
}
@@ -15,180 +15,173 @@ import java.util.*;
import static com.jetbrains.python.psi.PyUtil.as;
public final class PyPatternAnnotator extends PyAnnotatorBase {
final class PyPatternAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyPatternAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
public void visitPySingleStarPattern(@NotNull PySingleStarPattern starPattern) {
PsiElement parent = starPattern.getParent();
if (!(parent instanceof PySequencePattern)) {
myHolder.markError(starPattern, PyPsiBundle.message("ANN.patterns.single.star.pattern.cannot.be.used.outside.sequence.patterns"));
}
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
@Override
public void visitPyDoubleStarPattern(@NotNull PyDoubleStarPattern starPattern) {
PsiElement parent = starPattern.getParent();
if (!(parent instanceof PyMappingPattern)) {
myHolder.markError(starPattern, PyPsiBundle.message("ANN.patterns.double.star.pattern.cannot.be.used.outside.mapping.patterns"));
}
}
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void visitPyLiteralPattern(@NotNull PyLiteralPattern literalPattern) {
PyBinaryExpression expression = as(literalPattern.getExpression(), PyBinaryExpression.class);
if (expression != null) {
PyNumericLiteralExpression rightOperand = as(expression.getRightExpression(), PyNumericLiteralExpression.class);
if (rightOperand != null && rightOperand.getFirstChild().getNode().getElementType() != PyTokenTypes.IMAGINARY_LITERAL) {
myHolder.markError(expression, PyPsiBundle.message("ANN.patterns.invalid.complex.number.literal"));
}
}
}
@Override
public void visitPySingleStarPattern(@NotNull PySingleStarPattern starPattern) {
PsiElement parent = starPattern.getParent();
if (!(parent instanceof PySequencePattern)) {
myHolder.markError(starPattern, PyPsiBundle.message("ANN.patterns.single.star.pattern.cannot.be.used.outside.sequence.patterns"));
@Override
public void visitPyKeyValuePattern(@NotNull PyKeyValuePattern keyValuePattern) {
PyPattern keyPattern = keyValuePattern.getKeyPattern();
if (!(keyPattern instanceof PyValuePattern || keyPattern instanceof PyLiteralPattern)) {
myHolder.markError(keyPattern, PyPsiBundle.message("ANN.patterns.key.pattern.can.only.be.value.or.literal.pattern"));
}
}
@Override
public void visitPyOrPattern(@NotNull PyOrPattern orPattern) {
List<PyPattern> alternatives = orPattern.getAlternatives();
PyPattern lastAlternative = alternatives.get(alternatives.size() - 1);
Map<PyPattern, Set<String>> patternToBoundNames = new HashMap<>();
Set<String> allBoundNames = new HashSet<>();
for (PyPattern alternative : alternatives) {
Set<String> boundNames = SyntaxTraverser.psiTraverser(alternative)
.filter(PyTargetExpression.class)
.map(PyTargetExpression::getName)
.toSet();
patternToBoundNames.put(alternative, boundNames);
allBoundNames.addAll(boundNames);
if (alternative != lastAlternative && alternative.isIrrefutable()) {
myHolder.markError(alternative, PyPsiBundle.message("ANN.patterns.pattern.makes.remaining.alternatives.unreachable"));
}
}
@Override
public void visitPyDoubleStarPattern(@NotNull PyDoubleStarPattern starPattern) {
PsiElement parent = starPattern.getParent();
if (!(parent instanceof PyMappingPattern)) {
myHolder.markError(starPattern, PyPsiBundle.message("ANN.patterns.double.star.pattern.cannot.be.used.outside.mapping.patterns"));
for (Map.Entry<PyPattern, Set<String>> entry : patternToBoundNames.entrySet()) {
Set<String> boundNames = entry.getValue();
if (!boundNames.equals(allBoundNames)) {
Collection<String> missingNames = ContainerUtil.subtract(allBoundNames, boundNames);
String nameList = StringUtil.join(ContainerUtil.sorted(missingNames), ", ");
myHolder.markError(entry.getKey(), PyPsiBundle.message("ANN.patterns.pattern.does.not.bind.names", missingNames.size(), nameList));
}
}
}
@Override
public void visitPyLiteralPattern(@NotNull PyLiteralPattern literalPattern) {
PyBinaryExpression expression = as(literalPattern.getExpression(), PyBinaryExpression.class);
if (expression != null) {
PyNumericLiteralExpression rightOperand = as(expression.getRightExpression(), PyNumericLiteralExpression.class);
if (rightOperand != null && rightOperand.getFirstChild().getNode().getElementType() != PyTokenTypes.IMAGINARY_LITERAL) {
myHolder.markError(expression, PyPsiBundle.message("ANN.patterns.invalid.complex.number.literal"));
}
@Override
public void visitPyMatchStatement(@NotNull PyMatchStatement matchStatement) {
List<PyCaseClause> clauses = matchStatement.getCaseClauses();
if (clauses.isEmpty()) return;
for (PyCaseClause clause : clauses.subList(0, clauses.size() - 1)) {
PyPattern pattern = clause.getPattern();
if (pattern == null) {
continue;
}
PyPattern unwrappedPattern = unwrapGroupAndAsPatterns(pattern);
// There will be another warning for top-level star patterns
if (unwrappedPattern instanceof PySingleStarPattern || unwrappedPattern instanceof PyDoubleStarPattern) {
continue;
}
if (clause.getGuardCondition() == null && pattern.isIrrefutable()) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.pattern.makes.remaining.case.clauses.unreachable"));
}
}
}
@Override
public void visitPyKeyValuePattern(@NotNull PyKeyValuePattern keyValuePattern) {
PyPattern keyPattern = keyValuePattern.getKeyPattern();
if (!(keyPattern instanceof PyValuePattern || keyPattern instanceof PyLiteralPattern)) {
myHolder.markError(keyPattern, PyPsiBundle.message("ANN.patterns.key.pattern.can.only.be.value.or.literal.pattern"));
@Override
public void visitPyPatternArgumentList(@NotNull PyPatternArgumentList argumentList) {
Set<String> usedAttrNames = new HashSet<>();
for (PyPattern attrPattern : argumentList.getPatterns()) {
PyKeywordPattern keywordPattern = as(attrPattern, PyKeywordPattern.class);
if (keywordPattern == null) {
continue;
}
if (!usedAttrNames.add(keywordPattern.getKeyword())) {
myHolder.markError(keywordPattern.getKeywordElement(),
PyPsiBundle.message("ANN.patterns.attribute.name.is.repeated", keywordPattern.getKeyword()));
}
}
}
@Override
public void visitPyOrPattern(@NotNull PyOrPattern orPattern) {
List<PyPattern> alternatives = orPattern.getAlternatives();
PyPattern lastAlternative = alternatives.get(alternatives.size() - 1);
Map<PyPattern, Set<String>> patternToBoundNames = new HashMap<>();
Set<String> allBoundNames = new HashSet<>();
@Override
public void visitPyCaseClause(@NotNull PyCaseClause caseClause) {
PyPattern pattern = caseClause.getPattern();
if (pattern == null) return;
for (PyPattern alternative : alternatives) {
Set<String> boundNames = SyntaxTraverser.psiTraverser(alternative)
.filter(PyTargetExpression.class)
.map(PyTargetExpression::getName)
.toSet();
patternToBoundNames.put(alternative, boundNames);
allBoundNames.addAll(boundNames);
Stack<Set<String>> boundNamesPerOrBranch = new Stack<>();
// Assume that the top-level pattern is a single branch of a synthetic OR pattern
boundNamesPerOrBranch.push(new HashSet<>());
if (alternative != lastAlternative && alternative.isIrrefutable()) {
myHolder.markError(alternative, PyPsiBundle.message("ANN.patterns.pattern.makes.remaining.alternatives.unreachable"));
pattern.accept(new PyRecursiveElementVisitor() {
@Override
public void visitPyOrPattern(@NotNull PyOrPattern orPattern) {
Set<String> allBoundNamesInOrPattern = new HashSet<>();
for (PyPattern alternative : orPattern.getAlternatives()) {
boundNamesPerOrBranch.push(new HashSet<>());
alternative.accept(this);
allBoundNamesInOrPattern.addAll(boundNamesPerOrBranch.peek());
boundNamesPerOrBranch.pop();
}
boundNamesPerOrBranch.peek().addAll(allBoundNamesInOrPattern);
}
for (Map.Entry<PyPattern, Set<String>> entry : patternToBoundNames.entrySet()) {
Set<String> boundNames = entry.getValue();
if (!boundNames.equals(allBoundNames)) {
Collection<String> missingNames = ContainerUtil.subtract(allBoundNames, boundNames);
String nameList = StringUtil.join(ContainerUtil.sorted(missingNames), ", ");
myHolder.markError(entry.getKey(), PyPsiBundle.message("ANN.patterns.pattern.does.not.bind.names", missingNames.size(), nameList));
@Override
public void visitPyTargetExpression(@NotNull PyTargetExpression target) {
String name = target.getName();
boolean alreadyBound = ContainerUtil.exists(boundNamesPerOrBranch, names -> names.contains(name));
if (alreadyBound) {
myHolder.markError(target, PyPsiBundle.message("ANN.patterns.name.already.bound", name));
}
boundNamesPerOrBranch.peek().add(name);
}
});
}
@Override
public void visitPySequencePattern(@NotNull PySequencePattern sequencePattern) {
List<PySingleStarPattern> starPatterns = PsiTreeUtil.getChildrenOfTypeAsList(sequencePattern, PySingleStarPattern.class);
if (starPatterns.size() > 1) {
for (PySingleStarPattern pattern : starPatterns.subList(1, starPatterns.size())) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.repeated.star.pattern"));
}
}
}
@Override
public void visitPyMatchStatement(@NotNull PyMatchStatement matchStatement) {
List<PyCaseClause> clauses = matchStatement.getCaseClauses();
if (clauses.isEmpty()) return;
for (PyCaseClause clause : clauses.subList(0, clauses.size() - 1)) {
PyPattern pattern = clause.getPattern();
if (pattern == null) {
continue;
}
PyPattern unwrappedPattern = unwrapGroupAndAsPatterns(pattern);
// There will be another warning for top-level star patterns
if (unwrappedPattern instanceof PySingleStarPattern || unwrappedPattern instanceof PyDoubleStarPattern) {
continue;
}
if (clause.getGuardCondition() == null && pattern.isIrrefutable()) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.pattern.makes.remaining.case.clauses.unreachable"));
}
@Override
public void visitPyMappingPattern(@NotNull PyMappingPattern mappingPattern) {
List<PyDoubleStarPattern> starPatterns = PsiTreeUtil.getChildrenOfTypeAsList(mappingPattern, PyDoubleStarPattern.class);
if (starPatterns.size() > 1) {
for (PyDoubleStarPattern pattern : starPatterns.subList(1, starPatterns.size())) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.repeated.star.pattern"));
}
}
}
@Override
public void visitPyPatternArgumentList(@NotNull PyPatternArgumentList argumentList) {
Set<String> usedAttrNames = new HashSet<>();
for (PyPattern attrPattern : argumentList.getPatterns()) {
PyKeywordPattern keywordPattern = as(attrPattern, PyKeywordPattern.class);
if (keywordPattern == null) {
continue;
}
if (!usedAttrNames.add(keywordPattern.getKeyword())) {
myHolder.markError(keywordPattern.getKeywordElement(),
PyPsiBundle.message("ANN.patterns.attribute.name.is.repeated", keywordPattern.getKeyword()));
}
}
private static @NotNull PyPattern unwrapGroupAndAsPatterns(@NotNull PyPattern pattern) {
if (pattern instanceof PyGroupPattern) {
return unwrapGroupAndAsPatterns(((PyGroupPattern)pattern).getPattern());
}
@Override
public void visitPyCaseClause(@NotNull PyCaseClause caseClause) {
PyPattern pattern = caseClause.getPattern();
if (pattern == null) return;
Stack<Set<String>> boundNamesPerOrBranch = new Stack<>();
// Assume that the top-level pattern is a single branch of a synthetic OR pattern
boundNamesPerOrBranch.push(new HashSet<>());
pattern.accept(new PyRecursiveElementVisitor() {
@Override
public void visitPyOrPattern(@NotNull PyOrPattern orPattern) {
Set<String> allBoundNamesInOrPattern = new HashSet<>();
for (PyPattern alternative : orPattern.getAlternatives()) {
boundNamesPerOrBranch.push(new HashSet<>());
alternative.accept(this);
allBoundNamesInOrPattern.addAll(boundNamesPerOrBranch.peek());
boundNamesPerOrBranch.pop();
}
boundNamesPerOrBranch.peek().addAll(allBoundNamesInOrPattern);
}
@Override
public void visitPyTargetExpression(@NotNull PyTargetExpression target) {
String name = target.getName();
boolean alreadyBound = ContainerUtil.exists(boundNamesPerOrBranch, names -> names.contains(name));
if (alreadyBound) {
myHolder.markError(target, PyPsiBundle.message("ANN.patterns.name.already.bound", name));
}
boundNamesPerOrBranch.peek().add(name);
}
});
}
@Override
public void visitPySequencePattern(@NotNull PySequencePattern sequencePattern) {
List<PySingleStarPattern> starPatterns = PsiTreeUtil.getChildrenOfTypeAsList(sequencePattern, PySingleStarPattern.class);
if (starPatterns.size() > 1) {
for (PySingleStarPattern pattern : starPatterns.subList(1, starPatterns.size())) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.repeated.star.pattern"));
}
}
}
@Override
public void visitPyMappingPattern(@NotNull PyMappingPattern mappingPattern) {
List<PyDoubleStarPattern> starPatterns = PsiTreeUtil.getChildrenOfTypeAsList(mappingPattern, PyDoubleStarPattern.class);
if (starPatterns.size() > 1) {
for (PyDoubleStarPattern pattern : starPatterns.subList(1, starPatterns.size())) {
myHolder.markError(pattern, PyPsiBundle.message("ANN.patterns.repeated.star.pattern"));
}
}
}
private static @NotNull PyPattern unwrapGroupAndAsPatterns(@NotNull PyPattern pattern) {
if (pattern instanceof PyGroupPattern) {
return unwrapGroupAndAsPatterns(((PyGroupPattern)pattern).getPattern());
}
if (pattern instanceof PyAsPattern) {
return unwrapGroupAndAsPatterns(((PyAsPattern)pattern).getPattern());
}
return pattern;
if (pattern instanceof PyAsPattern) {
return unwrapGroupAndAsPatterns(((PyAsPattern)pattern).getPattern());
}
return pattern;
}
}
@@ -25,45 +25,38 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class PyStarAnnotator extends PyAnnotatorBase {
final class PyStarAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyStarAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
public void visitPyStarExpression(@NotNull PyStarExpression node) {
super.visitPyStarExpression(node);
PsiElement parent = node.getParent();
if (!node.isAssignmentTarget() &&
!allowedUnpacking(node) &&
!(parent instanceof PyParameterTypeList) &&
!(parent instanceof PyTypeParameter) &&
!(parent instanceof PyAnnotation && isVariadicArg(parent.getParent()))) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.can.t.use.starred.expression.here")).create();
}
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void visitPyStarExpression(@NotNull PyStarExpression node) {
super.visitPyStarExpression(node);
PsiElement parent = node.getParent();
if (!node.isAssignmentTarget() &&
!allowedUnpacking(node) &&
!(parent instanceof PyParameterTypeList) &&
!(parent instanceof PyTypeParameter) &&
!(parent instanceof PyAnnotation && isVariadicArg(parent.getParent()))) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.can.t.use.starred.expression.here")).create();
}
private static boolean allowedUnpacking(@NotNull PyStarExpression starExpression) {
if (!starExpression.isUnpacking()) {
return false;
}
private static boolean allowedUnpacking(@NotNull PyStarExpression starExpression) {
if (!starExpression.isUnpacking()) {
// Additional contexts where unpacking is prohibited depending on the language version are covered in CompatibilityVisitor.
final PsiElement parent = PsiTreeUtil.skipParentsOfType(starExpression, PyParenthesizedExpression.class);
if (parent instanceof PyTupleExpression) {
final PsiElement tupleParent = parent.getParent();
if (tupleParent instanceof PyYieldExpression && ((PyYieldExpression)tupleParent).isDelegating()) {
return false;
}
// Additional contexts where unpacking is prohibited depending on the language version are covered in CompatibilityVisitor.
final PsiElement parent = PsiTreeUtil.skipParentsOfType(starExpression, PyParenthesizedExpression.class);
if (parent instanceof PyTupleExpression) {
final PsiElement tupleParent = parent.getParent();
if (tupleParent instanceof PyYieldExpression && ((PyYieldExpression)tupleParent).isDelegating()) {
return false;
}
}
return true;
}
return true;
}
public static boolean isVariadicArg(@Nullable PsiElement parameter) {
@@ -5,7 +5,6 @@ import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.psi.PyElementVisitor;
@@ -19,82 +18,75 @@ import java.util.List;
/**
* Looks for well-formedness of string constants.
*/
public final class PyStringLiteralQuotesAnnotator extends PyAnnotatorBase {
final class PyStringLiteralQuotesAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyStringLiteralQuotesAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
private static final String TRIPLE_QUOTES = "\"\"\"";
private static final String TRIPLE_APOS = "'''";
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
public void visitPyStringLiteralExpression(final @NotNull PyStringLiteralExpression node) {
final List<ASTNode> stringNodes = node.getStringNodes();
for (ASTNode stringNode : stringNodes) {
// TODO Migrate to newer PyStringElement API
if (stringNode.getElementType() == PyElementTypes.FSTRING_NODE) {
continue;
}
final String nodeText = PyPsiUtils.getElementTextWithoutHostEscaping(stringNode.getPsi());
final int index = PyStringLiteralUtil.getPrefixLength(nodeText);
final String unprefixed = nodeText.substring(index);
final boolean foundError;
if (StringUtil.startsWith(unprefixed, TRIPLE_QUOTES)) {
foundError = checkTripleQuotedString(stringNode, unprefixed, TRIPLE_QUOTES);
}
else if (StringUtil.startsWith(unprefixed, TRIPLE_APOS)) {
foundError = checkTripleQuotedString(stringNode, unprefixed, TRIPLE_APOS);
}
else {
foundError = checkQuotedString(stringNode, unprefixed);
}
if (foundError) {
break;
}
}
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
private static final String TRIPLE_QUOTES = "\"\"\"";
private static final String TRIPLE_APOS = "'''";
@Override
public void visitPyStringLiteralExpression(final @NotNull PyStringLiteralExpression node) {
final List<ASTNode> stringNodes = node.getStringNodes();
for (ASTNode stringNode : stringNodes) {
// TODO Migrate to newer PyStringElement API
if (stringNode.getElementType() == PyElementTypes.FSTRING_NODE) {
continue;
}
final String nodeText = PyPsiUtils.getElementTextWithoutHostEscaping(stringNode.getPsi());
final int index = PyStringLiteralUtil.getPrefixLength(nodeText);
final String unprefixed = nodeText.substring(index);
final boolean foundError;
if (StringUtil.startsWith(unprefixed, TRIPLE_QUOTES)) {
foundError = checkTripleQuotedString(stringNode, unprefixed, TRIPLE_QUOTES);
}
else if (StringUtil.startsWith(unprefixed, TRIPLE_APOS)) {
foundError = checkTripleQuotedString(stringNode, unprefixed, TRIPLE_APOS);
}
else {
foundError = checkQuotedString(stringNode, unprefixed);
}
if (foundError) {
break;
}
private boolean checkQuotedString(@NotNull ASTNode stringNode, @NotNull String nodeText) {
final char firstQuote = nodeText.charAt(0);
final char lastChar = nodeText.charAt(nodeText.length() - 1);
int precedingBackslashCount = 0;
for (int i = nodeText.length() - 2; i >= 0; i--) {
if (nodeText.charAt(i) == '\\') {
precedingBackslashCount++;
}
else {
break;
}
}
private boolean checkQuotedString(@NotNull ASTNode stringNode, @NotNull String nodeText) {
final char firstQuote = nodeText.charAt(0);
final char lastChar = nodeText.charAt(nodeText.length() - 1);
int precedingBackslashCount = 0;
for (int i = nodeText.length() - 2; i >= 0; i--) {
if (nodeText.charAt(i) == '\\') {
precedingBackslashCount++;
}
else {
break;
}
}
if (nodeText.length() == 1 || lastChar != firstQuote || precedingBackslashCount % 2 != 0) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.missing.closing.quote", firstQuote)).range(stringNode)
.create();
return true;
}
return false;
if (nodeText.length() == 1 || lastChar != firstQuote || precedingBackslashCount % 2 != 0) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.missing.closing.quote", firstQuote)).range(stringNode)
.create();
return true;
}
return false;
}
private boolean checkTripleQuotedString(@NotNull ASTNode stringNode, @NotNull String text, @NotNull String quotes) {
if (text.length() < 6 || !text.endsWith(quotes)) {
int startOffset = StringUtil.trimTrailing(stringNode.getText()).lastIndexOf('\n');
if (startOffset < 0) {
startOffset = stringNode.getTextRange().getStartOffset();
}
else {
startOffset = stringNode.getTextRange().getStartOffset() + startOffset + 1;
}
final TextRange highlightRange = new TextRange(startOffset, stringNode.getTextRange().getEndOffset());
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.missing.closing.triple.quotes")).range(highlightRange)
.create();
return true;
private boolean checkTripleQuotedString(@NotNull ASTNode stringNode, @NotNull String text, @NotNull String quotes) {
if (text.length() < 6 || !text.endsWith(quotes)) {
int startOffset = StringUtil.trimTrailing(stringNode.getText()).lastIndexOf('\n');
if (startOffset < 0) {
startOffset = stringNode.getTextRange().getStartOffset();
}
return false;
else {
startOffset = stringNode.getTextRange().getStartOffset() + startOffset + 1;
}
final TextRange highlightRange = new TextRange(startOffset, stringNode.getTextRange().getEndOffset());
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.missing.closing.triple.quotes")).range(highlightRange)
.create();
return true;
}
return false;
}
}
@@ -21,7 +21,14 @@ public final class PySyntaxAnnotator extends PyAnnotatorBase implements DumbAwar
new PyGlobalAnnotatorVisitor(holder),
new PyImportAnnotatorVisitor(holder),
new PyAsyncAwaitAnnotatorVisitor(holder),
new PyAstNumericLiteralAnnotatorVisitor(holder)
new PyAstNumericLiteralAnnotatorVisitor(holder),
new PyGeneratorInArgumentListAnnotator(holder),
new PyStarAnnotator(holder),
new PyStringLiteralQuotesAnnotator(holder),
new PyFStringsAnnotator(holder),
new PyPatternAnnotator(holder),
new PyTryExceptAnnotator(holder),
new PyTypeParameterListAnnotator(holder)
);
for (PsiElementVisitor visitor : visitors) {
psiElement.accept(visitor);
@@ -11,116 +11,109 @@ import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class PyTryExceptAnnotator extends PyAnnotatorBase {
final class PyTryExceptAnnotator extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
PyTryExceptAnnotator(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void annotate(@NotNull PsiElement element, @NotNull PyAnnotationHolder holder) {
element.accept(new MyVisitor(holder));
public void visitPyTryExceptStatement(@NotNull PyTryExceptStatement node) {
boolean haveDefaultExcept = false;
for (PyExceptPart part : node.getExceptParts()) {
if (haveDefaultExcept) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.default.except.must.be.last")).range(part).create();
}
if (part.getExceptClass() == null) {
haveDefaultExcept = true;
}
}
boolean haveStar = false;
boolean haveNotStar = false;
for (PyExceptPart exceptPart : node.getExceptParts()) {
var star = PyPsiUtils.getFirstChildOfType(exceptPart, PyTokenTypes.MULT);
if (star != null) {
haveStar = true;
}
else {
haveNotStar = true;
}
if (haveNotStar && haveStar) {
var exceptKeyword = exceptPart.getFirstChild();
int startOffset = exceptKeyword.getTextOffset();
int endOffset = exceptKeyword.getTextRange().getEndOffset();
if (star != null) {
endOffset = star.getTextRange().getEndOffset();
}
var textRange = new TextRange(startOffset, endOffset);
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.try.except.can.not.have.except.and.star.except"))
.range(textRange).create();
break;
}
}
}
private static class MyVisitor extends PyElementVisitor {
private final @NotNull PyAnnotationHolder myHolder;
@Override
public void visitPyRaiseStatement(@NotNull PyRaiseStatement node) {
if (node.getExpressions().length == 0 &&
PsiTreeUtil.getParentOfType(node, PyExceptPart.class, PyFinallyPart.class, PyFunction.class) == null) {
myHolder.markError(node, PyPsiBundle.message("ANN.no.exception.to.reraise"));
}
}
private MyVisitor(@NotNull PyAnnotationHolder holder) { myHolder = holder; }
@Override
public void visitPyTryExceptStatement(@NotNull PyTryExceptStatement node) {
boolean haveDefaultExcept = false;
for (PyExceptPart part : node.getExceptParts()) {
if (haveDefaultExcept) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.default.except.must.be.last")).range(part).create();
}
if (part.getExceptClass() == null) {
haveDefaultExcept = true;
}
}
boolean haveStar = false;
boolean haveNotStar = false;
for (PyExceptPart exceptPart : node.getExceptParts()) {
var star = PyPsiUtils.getFirstChildOfType(exceptPart, PyTokenTypes.MULT);
if (star != null) {
haveStar = true;
}
else {
haveNotStar = true;
}
if (haveNotStar && haveStar) {
var exceptKeyword = exceptPart.getFirstChild();
int startOffset = exceptKeyword.getTextOffset();
int endOffset = exceptKeyword.getTextRange().getEndOffset();
if (star != null) {
endOffset = star.getTextRange().getEndOffset();
}
var textRange = new TextRange(startOffset, endOffset);
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.try.except.can.not.have.except.and.star.except"))
.range(textRange).create();
break;
private static @Nullable PyReferenceExpression tryGetExceptionGroupInExpression(@Nullable PsiElement exceptExpression) {
if (exceptExpression instanceof PyReferenceExpression &&
"ExceptionGroup".equals(((PyReferenceExpression)exceptExpression).getName())) {
return (PyReferenceExpression)exceptExpression;
}
if (exceptExpression instanceof PyParenthesizedExpression) {
return tryGetExceptionGroupInExpression(PyPsiUtils.flattenParens((PyParenthesizedExpression)exceptExpression));
}
if (exceptExpression instanceof PyTupleExpression) {
for (PsiElement child : exceptExpression.getChildren()) {
var result = tryGetExceptionGroupInExpression(child);
if (result != null) {
return result;
}
}
}
return null;
}
@Override
public void visitPyRaiseStatement(@NotNull PyRaiseStatement node) {
if (node.getExpressions().length == 0 &&
PsiTreeUtil.getParentOfType(node, PyExceptPart.class, PyFinallyPart.class, PyFunction.class) == null) {
myHolder.markError(node, PyPsiBundle.message("ANN.no.exception.to.reraise"));
}
@Override
public void visitPyExceptBlock(@NotNull PyExceptPart node) {
if (!node.isStar()) return;
var exceptClass = node.getExceptClass();
var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
if (exceptionGroup != null) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup)
.create();
}
}
private static @Nullable PyReferenceExpression tryGetExceptionGroupInExpression(@Nullable PsiElement exceptExpression) {
if (exceptExpression instanceof PyReferenceExpression &&
"ExceptionGroup".equals(((PyReferenceExpression)exceptExpression).getName())) {
return (PyReferenceExpression)exceptExpression;
}
if (exceptExpression instanceof PyParenthesizedExpression) {
return tryGetExceptionGroupInExpression(PyPsiUtils.flattenParens((PyParenthesizedExpression)exceptExpression));
}
if (exceptExpression instanceof PyTupleExpression) {
for (PsiElement child : exceptExpression.getChildren()) {
var result = tryGetExceptionGroupInExpression(child);
if (result != null) {
return result;
}
}
}
return null;
@Override
public void visitPyReturnStatement(@NotNull PyReturnStatement node) {
PyExceptPart exceptPart = PsiTreeUtil.getParentOfType(node, PyExceptPart.class, false, PyFunction.class);
if (exceptPart != null && exceptPart.isStar()) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.continue.break.or.return.in.star.except")).create();
}
}
@Override
public void visitPyExceptBlock(@NotNull PyExceptPart node) {
if (!node.isStar()) return;
var exceptClass = node.getExceptClass();
var exceptionGroup = tryGetExceptionGroupInExpression(exceptClass);
if (exceptionGroup != null) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.exception.group.in.star.except")).range(exceptionGroup)
.create();
}
private void checkForContinueAndReturn(@NotNull PsiElement node) {
PyExceptPart exceptPart = PsiTreeUtil.getParentOfType(node, PyExceptPart.class, false, PyLoopStatement.class);
if (exceptPart != null && exceptPart.isStar()) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.continue.break.or.return.in.star.except")).create();
}
}
@Override
public void visitPyReturnStatement(@NotNull PyReturnStatement node) {
PyExceptPart exceptPart = PsiTreeUtil.getParentOfType(node, PyExceptPart.class, false, PyFunction.class);
if (exceptPart != null && exceptPart.isStar()) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.continue.break.or.return.in.star.except")).create();
}
}
@Override
public void visitPyContinueStatement(@NotNull PyContinueStatement node) {
checkForContinueAndReturn(node);
}
private void checkForContinueAndReturn(@NotNull PsiElement node) {
PyExceptPart exceptPart = PsiTreeUtil.getParentOfType(node, PyExceptPart.class, false, PyLoopStatement.class);
if (exceptPart != null && exceptPart.isStar()) {
myHolder.newAnnotation(HighlightSeverity.ERROR, PyPsiBundle.message("ANN.continue.break.or.return.in.star.except")).create();
}
}
@Override
public void visitPyContinueStatement(@NotNull PyContinueStatement node) {
checkForContinueAndReturn(node);
}
@Override
public void visitPyBreakStatement(@NotNull PyBreakStatement node) {
checkForContinueAndReturn(node);
}
@Override
public void visitPyBreakStatement(@NotNull PyBreakStatement node) {
checkForContinueAndReturn(node);
}
}
@@ -1,7 +1,6 @@
package com.jetbrains.python.validation
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.psi.PsiElement
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.ast.PyAstTypeParameter.Kind.TypeVar
import com.jetbrains.python.psi.PyElementVisitor
@@ -10,13 +9,7 @@ import com.jetbrains.python.psi.PyTypeParameter
import com.jetbrains.python.psi.PyTypeParameterList
import com.jetbrains.python.psi.impl.PyPsiUtils
class PyTypeParameterListAnnotator : PyAnnotatorBase() {
override fun annotate(element: PsiElement, holder: PyAnnotationHolder) {
element.accept(PyTypeParameterListAnnotatorVisitor(holder))
}
}
private class PyTypeParameterListAnnotatorVisitor(private val holder: PyAnnotationHolder) : PyElementVisitor() {
class PyTypeParameterListAnnotator(private val holder: PyAnnotationHolder) : PyElementVisitor() {
override fun visitPyTypeParameterList(node: PyTypeParameterList) {
if (!node.typeParameters.isEmpty()) {
val namesSet = mutableSetOf<String>()