diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 338bdc5de6b8..321f9c747c00 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -871,6 +871,7 @@ python.colors.string.binary.bytes=String//Binary (bytes) python.colors.line.comment=Line Comment python.colors.keyword=Keyword python.colors.number=Number +python.colors.local.variables=Local variables python.new.project.synchronization.not.configured.dialog.title=Synchronization not Configured python.new.project.synchronization.not.configured.dialog.message=Local/Remote synchronization is not configured correctly.\n{0}\n\ diff --git a/python/python-psi-impl/src/com/jetbrains/python/highlighting/PyHighlighter.java b/python/python-psi-impl/src/com/jetbrains/python/highlighting/PyHighlighter.java index aad4f5985d46..1fd85eb10a06 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/highlighting/PyHighlighter.java +++ b/python/python-psi-impl/src/com/jetbrains/python/highlighting/PyHighlighter.java @@ -119,6 +119,7 @@ public class PyHighlighter extends SyntaxHighlighterBase { public static final TextAttributesKey PY_FSTRING_FRAGMENT_BRACES = TextAttributesKey.createTextAttributesKey("PY.FSTRING_FRAGMENT_BRACES", VALID_STRING_ESCAPE); public static final TextAttributesKey PY_FSTRING_FRAGMENT_COLON = TextAttributesKey.createTextAttributesKey("PY.FSTRING_FRAGMENT_COLON", VALID_STRING_ESCAPE); public static final TextAttributesKey PY_FSTRING_FRAGMENT_TYPE_CONVERSION = TextAttributesKey.createTextAttributesKey("PY.FSTRING_FRAGMENT_TYPE_CONVERSION", VALID_STRING_ESCAPE); + public static final TextAttributesKey PY_LOCAL_VARIABLE = TextAttributesKey.createTextAttributesKey("PY.LOCAL_VARIABLE", LOCAL_VARIABLE); /** * The 'heavy' constructor that initializes everything. PySyntaxHighlighterFactory caches such instances per level. diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/PyVariableAnnotator.kt b/python/python-psi-impl/src/com/jetbrains/python/validation/PyVariableAnnotator.kt new file mode 100644 index 000000000000..831217ed5896 --- /dev/null +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/PyVariableAnnotator.kt @@ -0,0 +1,54 @@ +package com.jetbrains.python.validation + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil +import com.jetbrains.python.highlighting.PyHighlighter +import com.jetbrains.python.psi.* +import com.jetbrains.python.psi.resolve.PyResolveUtil + +class PyVariableAnnotator : PyAnnotator() { + + override fun visitPyTargetExpression(node: PyTargetExpression) { + if (node.isNonLocalOrGlobal() || node.isQualified) return + + val scopeOwner = ScopeUtil.getScopeOwner(node) + if (node.parent is PyAssignmentStatement && scopeOwner is PyFunction) { + node.nameElement?.let { addHighlightingAnnotation(it, PyHighlighter.PY_LOCAL_VARIABLE) } + } + } + + override fun visitPyReferenceExpression(node: PyReferenceExpression) { + PyResolveUtil.resolveLocally(node) + .filterIsInstance(PyTargetExpression::class.java) + .forEach { expression -> + if (ScopeUtil.getScopeOwner(expression) is PyFunction && !expression.isNonLocalOrGlobal()) { + addHighlightingAnnotation(node.node, PyHighlighter.PY_LOCAL_VARIABLE) + } + } + } + + private fun PyQualifiedExpression.isNonLocalOrGlobal(): Boolean { + val qName = this.asQualifiedName() + val scopeOwner = ScopeUtil.getScopeOwner(this) + + if (qName != null && scopeOwner is PyFunction) { + val scopesToLookUp = mutableListOf(scopeOwner) + + scopesToLookUp.addAll(PsiTreeUtil.findChildrenOfType(scopeOwner, PyFunction::class.java)) + scopesToLookUp.forEach { scope -> + if (PyResolveUtil.resolveLocally(scope, qName.toString()).containsNonLocalOrGlobal()) { + return true + } + } + } + return false + } + + private fun Collection.containsNonLocalOrGlobal(): Boolean = + this.filterIsInstance(PyTargetExpression::class.java) + .any { expression -> expression.parent.isNonLocalOrGlobal() } + + private fun PsiElement.isNonLocalOrGlobal(): Boolean = + this is PyNonlocalStatement || this is PyGlobalStatement +} \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 68ab5442a039..917d6b866af4 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -632,6 +632,7 @@ + diff --git a/python/src/com/jetbrains/python/highlighting/PythonColorsPage.java b/python/src/com/jetbrains/python/highlighting/PythonColorsPage.java index b6502ebb8460..d28c600b154a 100644 --- a/python/src/com/jetbrains/python/highlighting/PythonColorsPage.java +++ b/python/src/com/jetbrains/python/highlighting/PythonColorsPage.java @@ -5,7 +5,6 @@ import com.google.common.collect.ImmutableMap; import com.intellij.application.options.colors.InspectionColorSettingsPage; import com.intellij.codeHighlighting.RainbowHighlighter; import com.intellij.lang.Language; -import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; @@ -68,6 +67,7 @@ public class PythonColorsPage implements RainbowColorSettingsPage, InspectionCol new AttributesDescriptor(PyBundle.message("python.colors.decorator"), PyHighlighter.PY_DECORATOR), new AttributesDescriptor(PyBundle.message("python.colors.class.definition"), PyHighlighter.PY_CLASS_DEFINITION), new AttributesDescriptor(PyBundle.message("python.colors.type.annotation"), PyHighlighter.PY_ANNOTATION), + new AttributesDescriptor(PyBundle.message("python.colors.local.variables"), PyHighlighter.PY_LOCAL_VARIABLE), }; @NonNls private static final Map ourTagToDescriptorMap = ImmutableMap.builder() @@ -86,7 +86,7 @@ public class PythonColorsPage implements RainbowColorSettingsPage, InspectionCol .put("call", PyHighlighter.PY_FUNCTION_CALL) .put("mcall", PyHighlighter.PY_METHOD_CALL) .put("annotation", PyHighlighter.PY_ANNOTATION) - .put("localVar", DefaultLanguageHighlighterColors.LOCAL_VARIABLE) + .put("localVar", PyHighlighter.PY_LOCAL_VARIABLE) .putAll(RainbowHighlighter.createRainbowHLM()) .build(); diff --git a/python/testData/highlighting/localVariables.py b/python/testData/highlighting/localVariables.py new file mode 100644 index 000000000000..494891d8a1b5 --- /dev/null +++ b/python/testData/highlighting/localVariables.py @@ -0,0 +1,8 @@ +def fun(): + local_var = "hello" + print(local_var) + + def nested(): + print(local_var) + + return \ No newline at end of file diff --git a/python/testData/highlighting/nestedParamHighlightingInInnerFunc.py b/python/testData/highlighting/nestedParamHighlightingInInnerFunc.py index 06c9678dba16..dfc09c48b567 100644 --- a/python/testData/highlighting/nestedParamHighlightingInInnerFunc.py +++ b/python/testData/highlighting/nestedParamHighlightingInInnerFunc.py @@ -1,8 +1,8 @@ def outer_func(a, b): def inner_func_one(c): def inner_func_two(d): - x = 10 - return a + b + c + d + x + x = 10 + return a + b + c + d + x return inner_func_two(4) diff --git a/python/testData/highlighting/variableAnnotatedWithGlobalNotHighlightedAsLocal.py b/python/testData/highlighting/variableAnnotatedWithGlobalNotHighlightedAsLocal.py new file mode 100644 index 000000000000..4a37915061fe --- /dev/null +++ b/python/testData/highlighting/variableAnnotatedWithGlobalNotHighlightedAsLocal.py @@ -0,0 +1,5 @@ +def foo(): + global g + g = "world!" + +print("Hello, " + g) \ No newline at end of file diff --git a/python/testData/highlighting/variableAnnotatedWithNonLocalNotHighlightedAsLocal.py b/python/testData/highlighting/variableAnnotatedWithNonLocalNotHighlightedAsLocal.py new file mode 100644 index 000000000000..14b497d68708 --- /dev/null +++ b/python/testData/highlighting/variableAnnotatedWithNonLocalNotHighlightedAsLocal.py @@ -0,0 +1,7 @@ +def outer(): + x = "John" + def inner(): + nonlocal x + x = "hello" + inner() + return x \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java index 2c8d335fda73..ed5881f9dca2 100644 --- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java @@ -565,6 +565,21 @@ public class PythonHighlightingTest extends PyTestCase { doTest(LanguageLevel.getLatest(), false, true); } + // PY-32302 + public void testLocalVariables() { + doTest(LanguageLevel.getLatest(), false, true); + } + + // PY-32302 + public void testVariableAnnotatedWithNonLocalNotHighlightedAsLocal() { + doTest(LanguageLevel.getLatest(), false, true); + } + + // PY-32302 + public void testVariableAnnotatedWithGlobalNotHighlightedAsLocal() { + doTest(LanguageLevel.getLatest(), false, true); + } + @NotNull private static EditorColorsScheme createTemporaryColorScheme() { EditorColorsManager manager = EditorColorsManager.getInstance();