diff --git a/python/src/com/jetbrains/python/inspections/PyInconsistentIndentationInspection.java b/python/src/com/jetbrains/python/inspections/PyInconsistentIndentationInspection.java new file mode 100644 index 000000000000..a3b5c41a450d --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyInconsistentIndentationInspection.java @@ -0,0 +1,104 @@ +package com.jetbrains.python.inspections; + +import com.intellij.codeInspection.InspectionManager; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.lexer.PythonIndentingLexer; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author yole + */ +public class PyInconsistentIndentationInspection extends PyInspection { + @Nls + @NotNull + @Override + public String getDisplayName() { + return "Inconsistent indentation"; + } + + @Override + public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) { + return new IndentValidator(file, manager, isOnTheFly).invoke(); + } + + private static class IndentValidator { + private PsiFile myFile; + private InspectionManager myManager; + private boolean myOnTheFly; + private List myProblems; + private int myLastTabs = 0; + private int myLastSpaces = 0; + + public IndentValidator(PsiFile file, InspectionManager manager, boolean isOnTheFly) { + myFile = file; + myManager = manager; + myOnTheFly = isOnTheFly; + myProblems = new ArrayList(); + } + + public ProblemDescriptor[] invoke() { + PythonIndentingLexer lexer = new PythonIndentingLexer(); + final String text = myFile.getText(); + lexer.start(text); + while (lexer.getTokenType() != null) { + final IElementType tokenType = lexer.getTokenType(); + if (tokenType == PyTokenTypes.STATEMENT_BREAK) { + lexer.advance(); + while(lexer.getTokenType() != null && lexer.getTokenType() != PyTokenTypes.LINE_BREAK) { + lexer.advance(); + } + if (lexer.getTokenType() == PyTokenTypes.LINE_BREAK) { + String indent = text.substring(lexer.getTokenStart(), lexer.getTokenEnd()); + validateIndent(lexer.getTokenStart(), indent); + } + } + lexer.advance(); + } + return myProblems.toArray(new ProblemDescriptor[myProblems.size()]); + } + + private void validateIndent(final int tokenStart, String indent) { + int lastLF = indent.lastIndexOf('\n'); + String lastLineIndent = indent.substring(lastLF+1); + int spaces = 0; + int tabs = 0; + final int length = lastLineIndent.length(); + for (int i = 0; i < length; i++) { + final char c = lastLineIndent.charAt(i); + if (c == ' ') spaces++; + else if (c == '\t') tabs++; + } + final int problemStart = tokenStart + lastLF + 1; + if (spaces > 0 && tabs > 0) { + reportProblem("Inconsistent indentation: mix of tabs and spaces", problemStart, length); + } + else if (spaces > 0 && myLastTabs > 0) { + reportProblem("Inconsistent indentation: previous line used tabs, this line uses spaces", problemStart, length); + } + else if (tabs > 0 && myLastSpaces > 0) { + reportProblem("Inconsistent indentation: previous line used spaces, this line uses tabs", problemStart, length); + } + myLastTabs = tabs; + myLastSpaces = spaces; + } + + private void reportProblem(final String descriptionTemplate, final int problemStart, final int problemLength) { + PsiElement elt = myFile.findElementAt(problemStart); + int startOffset = problemStart - elt.getTextRange().getStartOffset(); + int endOffset = startOffset + problemLength; + myProblems.add(myManager.createProblemDescriptor(elt, new TextRange(startOffset, endOffset), + descriptionTemplate, + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myOnTheFly)); + } + } +} diff --git a/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java b/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java index 933453cc9f26..0ca08d2fe75c 100644 --- a/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java +++ b/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java @@ -43,6 +43,7 @@ public class PythonInspectionToolProvider implements InspectionToolProvider { PyCallingNonCallableInspection.class, PyPropertyAccessInspection.class, PyPropertyDefinitionInspection.class, + PyInconsistentIndentationInspection.class }; } } diff --git a/python/src/com/jetbrains/python/lexer/PythonIndentingLexer.java b/python/src/com/jetbrains/python/lexer/PythonIndentingLexer.java index ea2e4f1662e3..bc4310097459 100644 --- a/python/src/com/jetbrains/python/lexer/PythonIndentingLexer.java +++ b/python/src/com/jetbrains/python/lexer/PythonIndentingLexer.java @@ -2,27 +2,25 @@ package com.jetbrains.python.lexer; import com.intellij.psi.tree.IElementType; import com.jetbrains.python.PyTokenTypes; - -import java.util.Stack; +import gnu.trove.TIntStack; /** * @author yole */ public class PythonIndentingLexer extends PythonFutureAwareLexer { - private final Stack _indentStack = new Stack(); - private int _braceLevel; - private boolean _lineHasSignificantTokens; + private final TIntStack myIndentStack = new TIntStack(); + private int myBraceLevel; + private boolean myLineHasSignificantTokens; private static final boolean DUMP_TOKENS = false; - // used in Selena public void start(CharSequence buffer, int startOffset, int endOffset, int initialState) { checkStartState(startOffset, initialState); super.start(buffer, startOffset, endOffset, initialState); setStartState(); } - private void checkStartState(int startOffset, int initialState) { + private static void checkStartState(int startOffset, int initialState) { if (DUMP_TOKENS) { System.out.println("\n--- LEXER START---"); } @@ -32,27 +30,27 @@ public class PythonIndentingLexer extends PythonFutureAwareLexer { } private void setStartState() { - _indentStack.clear(); - _indentStack.push(0); - _braceLevel = 0; + myIndentStack.clear(); + myIndentStack.push(0); + myBraceLevel = 0; adjustBraceLevel(); - _lineHasSignificantTokens = false; + myLineHasSignificantTokens = false; checkSignificantTokens(); } private void adjustBraceLevel() { if (PyTokenTypes.OPEN_BRACES.contains(getTokenType())) { - _braceLevel++; + myBraceLevel++; } else if (PyTokenTypes.CLOSE_BRACES.contains(getTokenType())) { - _braceLevel--; + myBraceLevel--; } } private void checkSignificantTokens() { IElementType tokenType = getBaseTokenType(); if (!PyTokenTypes.WHITESPACE_OR_LINEBREAK.contains(tokenType) && tokenType != PyTokenTypes.END_OF_LINE_COMMENT) { - _lineHasSignificantTokens = true; + myLineHasSignificantTokens = true; } } @@ -70,7 +68,7 @@ public class PythonIndentingLexer extends PythonFutureAwareLexer { if (getTokenType() != null) { System.out.print(getTokenStart() + "-" + getTokenEnd() + ":" + getTokenType()); if (getTokenType() == PyTokenTypes.LINE_BREAK) { - System.out.println("{" + _braceLevel + "}"); + System.out.println("{" + myBraceLevel + "}"); } else { System.out.print(" "); @@ -127,11 +125,11 @@ public class PythonIndentingLexer extends PythonFutureAwareLexer { } private void processLineBreak(int startPos) { - if (_braceLevel == 0) { - if (_lineHasSignificantTokens) { + if (myBraceLevel == 0) { + if (myLineHasSignificantTokens) { pushToken(PyTokenTypes.STATEMENT_BREAK, startPos, startPos); } - _lineHasSignificantTokens = false; + myLineHasSignificantTokens = false; advanceBase(); processIndent(startPos); } @@ -160,7 +158,7 @@ public class PythonIndentingLexer extends PythonFutureAwareLexer { } private void processIndent(int whiteSpaceStart) { - int lastIndent = _indentStack.peek(); + int lastIndent = myIndentStack.peek(); int indent = getNextLineIndent(); // don't generate indent/dedent tokens if a line contains only end-of-line comment and whitespace if (getBaseTokenType() == PyTokenTypes.END_OF_LINE_COMMENT) { @@ -168,14 +166,14 @@ public class PythonIndentingLexer extends PythonFutureAwareLexer { } int whiteSpaceEnd = (getBaseTokenType() == null) ? super.getBufferEnd() : getBaseTokenStart(); if (indent > lastIndent) { - _indentStack.push(indent); + myIndentStack.push(indent); myTokenQueue.add(new PendingToken(PyTokenTypes.LINE_BREAK, whiteSpaceStart, whiteSpaceEnd)); myTokenQueue.add(new PendingToken(PyTokenTypes.INDENT, whiteSpaceEnd, whiteSpaceEnd)); } else if (indent < lastIndent) { while (indent < lastIndent) { - _indentStack.pop(); - lastIndent = _indentStack.peek(); + myIndentStack.pop(); + lastIndent = myIndentStack.peek(); if (indent > lastIndent) { myTokenQueue.add(new PendingToken(PyTokenTypes.INCONSISTENT_DEDENT, whiteSpaceStart, whiteSpaceStart)); } diff --git a/python/testData/inspections/InconsistentIndentation/test.py b/python/testData/inspections/InconsistentIndentation/test.py new file mode 100644 index 000000000000..300d0917ab1d --- /dev/null +++ b/python/testData/inspections/InconsistentIndentation/test.py @@ -0,0 +1,16 @@ +def foo(): + print "foo" + +def bar(): + print "foo" + print "bar" + +""" + foo + bar +""" + +print foo( + bar, + baz +) diff --git a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java index 348639116d49..94d90d747444 100644 --- a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java +++ b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java @@ -183,9 +183,7 @@ public class PythonInspectionsTest extends PyLightFixtureTestCase { } public void testPyFromFutureImportInspection() { - myFixture.configureByFile("inspections/" + getTestName(true) + "/test.py"); - myFixture.enableInspections(PyFromFutureImportInspection.class); - myFixture.checkHighlighting(true, false, false); + doHighlightingTest(PyFromFutureImportInspection.class); } public void testPyFromFutureImportInspectionDocString() { @@ -245,4 +243,8 @@ public class PythonInspectionsTest extends PyLightFixtureTestCase { public void testPyPropertyDefinitionInspection26() { doTestWithLanguageLevel(getTestName(false), new PyPropertyDefinitionInspection(), LanguageLevel.PYTHON26); } + + public void testInconsistentIndentation() { + doHighlightingTest(PyInconsistentIndentationInspection.class); + } }