diff --git a/python/psi-api/src/com/jetbrains/python/psi/LanguageLevel.java b/python/psi-api/src/com/jetbrains/python/psi/LanguageLevel.java index 741cb0b889a7..0d409ebe53f2 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/LanguageLevel.java +++ b/python/psi-api/src/com/jetbrains/python/psi/LanguageLevel.java @@ -91,6 +91,11 @@ public enum LanguageLevel { return mySupportsSetLiterals; } + public boolean isOutdatedPython2() { + return !myIsPy3K; + } + + @Deprecated public boolean isPy3K() { return myIsPy3K; } @@ -152,7 +157,7 @@ public enum LanguageLevel { public static LanguageLevel forElement(@NotNull PsiElement element) { final PsiFile containingFile = element.getContainingFile(); if (containingFile instanceof PyFile) { - return ((PyFile) containingFile).getLanguageLevel(); + return ((PyFile)containingFile).getLanguageLevel(); } return getDefault(); } diff --git a/python/src/com/jetbrains/python/parsing/ExpressionParsing.java b/python/src/com/jetbrains/python/parsing/ExpressionParsing.java index c9442ece255f..82295f3a2ec0 100644 --- a/python/src/com/jetbrains/python/parsing/ExpressionParsing.java +++ b/python/src/com/jetbrains/python/parsing/ExpressionParsing.java @@ -37,7 +37,7 @@ public class ExpressionParsing extends Parsing { public boolean parsePrimaryExpression(boolean isTargetExpression) { final IElementType firstToken = myBuilder.getTokenType(); - if (firstToken == PyTokenTypes.IDENTIFIER) { + if (isIdentifier(myBuilder)) { if (isTargetExpression) { buildTokenElement(PyElementTypes.TARGET_EXPRESSION, myBuilder); } @@ -303,6 +303,20 @@ public class ExpressionParsing extends Parsing { parseComprehension(expr, PyTokenTypes.RPAR, PyElementTypes.GENERATOR_EXPRESSION); } else { + final PsiBuilder.Marker err = myBuilder.mark(); + boolean empty = true; + while (myBuilder.getTokenType() != PyTokenTypes.RPAR && + myBuilder.getTokenType() != PyTokenTypes.LINE_BREAK && + myBuilder.getTokenType() != PyTokenTypes.STATEMENT_BREAK) { + myBuilder.advanceLexer(); + empty = false; + } + if (!empty) { + err.error("Unexpected expression syntax"); + } + else { + err.drop(); + } checkMatches(PyTokenTypes.RPAR, message("PARSE.expected.rpar")); expr.done(PyElementTypes.PARENTHESIZED_EXPRESSION); } @@ -323,7 +337,7 @@ public class ExpressionParsing extends Parsing { boolean recastFirstIdentifier = false; boolean recastQualifier = false; do { - boolean firstIdentifierIsTarget = isTargetExpression && ! recastFirstIdentifier; + boolean firstIdentifierIsTarget = isTargetExpression && !recastFirstIdentifier; PsiBuilder.Marker expr = myBuilder.mark(); if (!parsePrimaryExpression(firstIdentifierIsTarget)) { expr.drop(); @@ -340,7 +354,7 @@ public class ExpressionParsing extends Parsing { } myBuilder.advanceLexer(); checkMatches(PyTokenTypes.IDENTIFIER, message("PARSE.expected.name")); - if (isTargetExpression && ! recastQualifier && !atAnyOfTokens(PyTokenTypes.DOT, PyTokenTypes.LPAR, PyTokenTypes.LBRACKET)) { + if (isTargetExpression && !recastQualifier && !atAnyOfTokens(PyTokenTypes.DOT, PyTokenTypes.LPAR, PyTokenTypes.LBRACKET)) { expr.done(PyElementTypes.TARGET_EXPRESSION); } else { @@ -390,7 +404,7 @@ public class ExpressionParsing extends Parsing { expr.done(PyElementTypes.SUBSCRIPTION_EXPRESSION); } } - if (isTargetExpression && ! recastQualifier) { + if (isTargetExpression && !recastQualifier) { recastFirstIdentifier = true; // subscription is always a reference recastQualifier = true; // recast non-first qualifiers too expr.rollbackTo(); @@ -524,9 +538,9 @@ public class ExpressionParsing extends Parsing { starArgMarker.done(PyElementTypes.STAR_ARGUMENT_EXPRESSION); } else { - if (myBuilder.getTokenType() == PyTokenTypes.IDENTIFIER) { + if (isIdentifier(myBuilder)) { final PsiBuilder.Marker keywordArgMarker = myBuilder.mark(); - myBuilder.advanceLexer(); + advanceIdentifierLike(myBuilder); if (myBuilder.getTokenType() == PyTokenTypes.EQ) { myBuilder.advanceLexer(); if (!parseSingleExpression(false)) { @@ -637,7 +651,7 @@ public class ExpressionParsing extends Parsing { return parseLambdaExpression(false); } PsiBuilder.Marker condExpr = myBuilder.mark(); - if (!parseORTestExpression( stopOnIn, isTargetExpression)) { + if (!parseORTestExpression(stopOnIn, isTargetExpression)) { condExpr.drop(); return false; } @@ -982,7 +996,8 @@ public class ExpressionParsing extends Parsing { else { if (isTargetExpression) { expr.error("can't assign to await expression"); - } else { + } + else { expr.done(PyElementTypes.PREFIX_EXPRESSION); } } @@ -997,10 +1012,12 @@ public class ExpressionParsing extends Parsing { private boolean atForOrAsyncFor() { if (atToken(PyTokenTypes.FOR_KEYWORD)) { return true; - } else if (matchToken(PyTokenTypes.ASYNC_KEYWORD)) { + } + else if (matchToken(PyTokenTypes.ASYNC_KEYWORD)) { if (atToken(PyTokenTypes.FOR_KEYWORD)) { return true; - } else { + } + else { myBuilder.error("'for' expected"); return false; } diff --git a/python/src/com/jetbrains/python/parsing/FunctionParsing.java b/python/src/com/jetbrains/python/parsing/FunctionParsing.java index 3f90788f7e8e..8c2557493d37 100644 --- a/python/src/com/jetbrains/python/parsing/FunctionParsing.java +++ b/python/src/com/jetbrains/python/parsing/FunctionParsing.java @@ -24,6 +24,7 @@ import com.jetbrains.python.psi.LanguageLevel; import org.jetbrains.annotations.NotNull; import static com.jetbrains.python.PyBundle.message; +import static com.jetbrains.python.parsing.StatementParsing.TOK_ASYNC; /** * @author yole @@ -57,7 +58,10 @@ public class FunctionParsing extends Parsing { } public void parseReturnTypeAnnotation() { - if (myContext.getLanguageLevel().isPy3K() && myBuilder.getTokenType() == PyTokenTypes.RARROW) { + if (myBuilder.getTokenType() == PyTokenTypes.RARROW) { + if (myContext.getLanguageLevel().isOutdatedPython2()) { + myBuilder.error("Return type annotations are unsupported in Python 2"); + } PsiBuilder.Marker maybeReturnAnnotation = myBuilder.mark(); nextToken(); if (!myContext.getExpressionParser().parseSingleExpression(false)) { @@ -104,6 +108,10 @@ public class FunctionParsing extends Parsing { myBuilder.advanceLexer(); parseDeclarationAfterDecorator(endMarker, true); } + else if (atToken(PyTokenTypes.IDENTIFIER, TOK_ASYNC)) { + advanceAsync(true); + parseDeclarationAfterDecorator(endMarker, true); + } else { parseDeclarationAfterDecorator(endMarker, false); } @@ -186,15 +194,20 @@ public class FunctionParsing extends Parsing { } protected boolean parseParameter(IElementType endToken, boolean isLambda) { - final PsiBuilder.Marker parameter = myBuilder.mark(); + PsiBuilder.Marker parameter = myBuilder.mark(); boolean isStarParameter = false; if (myBuilder.getTokenType() == PyTokenTypes.MULT) { myBuilder.advanceLexer(); - if (myContext.getLanguageLevel().isPy3K() && - (myBuilder.getTokenType() == PyTokenTypes.COMMA) || myBuilder.getTokenType() == endToken) { + if ((myBuilder.getTokenType() == PyTokenTypes.COMMA) || myBuilder.getTokenType() == endToken) { + if (myContext.getLanguageLevel().isOutdatedPython2()) { + parameter.rollbackTo(); + parameter = myBuilder.mark(); + advanceError(myBuilder, "Single star parameter is not supported in Python 2"); + } parameter.done(PyElementTypes.SINGLE_STAR_PARAMETER); return true; } + isStarParameter = true; } else if (myBuilder.getTokenType() == PyTokenTypes.EXP) { @@ -208,7 +221,7 @@ public class FunctionParsing extends Parsing { if (!isStarParameter && matchToken(PyTokenTypes.EQ)) { if (!getExpressionParser().parseSingleExpression(false)) { PsiBuilder.Marker invalidElements = myBuilder.mark(); - while(!atAnyOfTokens(endToken, PyTokenTypes.LINE_BREAK, PyTokenTypes.COMMA, null)) { + while (!atAnyOfTokens(endToken, PyTokenTypes.LINE_BREAK, PyTokenTypes.COMMA, null)) { nextToken(); } invalidElements.error(message("PARSE.expected.expression")); @@ -232,7 +245,10 @@ public class FunctionParsing extends Parsing { } public void parseParameterAnnotation() { - if (myContext.getLanguageLevel().isPy3K() && atToken(PyTokenTypes.COLON)) { + if (atToken(PyTokenTypes.COLON)) { + if (myContext.getLanguageLevel().isOutdatedPython2()) { + myBuilder.error("Type annotations are unsupported in Python 2"); + } PsiBuilder.Marker annotationMarker = myBuilder.mark(); nextToken(); if (!getExpressionParser().parseSingleExpression(false)) { diff --git a/python/src/com/jetbrains/python/parsing/Parsing.java b/python/src/com/jetbrains/python/parsing/Parsing.java index ce8f409c3642..172fb93e5a87 100644 --- a/python/src/com/jetbrains/python/parsing/Parsing.java +++ b/python/src/com/jetbrains/python/parsing/Parsing.java @@ -110,9 +110,43 @@ public class Parsing { myBuilder.advanceLexer(); } + protected void advanceAsync(boolean falseAsync) { + if (falseAsync) { + advanceError(myBuilder, "'async' keyword is not expected here"); + } + else { + myBuilder.advanceLexer(); + } + } + + protected static void advanceIdentifierLike(@NotNull PsiBuilder builder) { + if (isFalseIdentifier(builder)) { + String tokenText = builder.getTokenText(); + advanceError(builder, "'" + tokenText + "' keyword can't be used as identifier in Python 2"); + } + else { + builder.advanceLexer(); + } + } + + protected static void advanceError(@NotNull PsiBuilder builder, String message) { + final PsiBuilder.Marker err = builder.mark(); + builder.advanceLexer(); + err.error(message); + } + + protected static boolean isIdentifier(@NotNull PsiBuilder builder) { + return builder.getTokenType() == PyTokenTypes.IDENTIFIER || isFalseIdentifier(builder); + } + + private static boolean isFalseIdentifier(@NotNull PsiBuilder builder) { + return builder.getTokenType() == PyTokenTypes.EXEC_KEYWORD || + builder.getTokenType() == PyTokenTypes.PRINT_KEYWORD; + } + protected static void buildTokenElement(IElementType type, PsiBuilder builder) { final PsiBuilder.Marker marker = builder.mark(); - builder.advanceLexer(); + advanceIdentifierLike(builder); marker.done(type); } diff --git a/python/src/com/jetbrains/python/parsing/StatementParsing.java b/python/src/com/jetbrains/python/parsing/StatementParsing.java index ec54d4755b5e..997547fb7e34 100644 --- a/python/src/com/jetbrains/python/parsing/StatementParsing.java +++ b/python/src/com/jetbrains/python/parsing/StatementParsing.java @@ -50,7 +50,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { @NonNls protected static final String TOK_FALSE = "False"; @NonNls protected static final String TOK_NONLOCAL = "nonlocal"; @NonNls protected static final String TOK_EXEC = "exec"; - @NonNls protected static final String TOK_ASYNC = "async"; + @NonNls public static final String TOK_ASYNC = "async"; @NonNls protected static final String TOK_AWAIT = "await"; private static final String EXPRESSION_EXPECTED = "Expression expected"; @@ -133,10 +133,16 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { return; } if (firstToken == PyTokenTypes.ASYNC_KEYWORD) { - parseAsyncStatement(); + parseAsyncStatement(false); return; } + if (atToken(PyTokenTypes.IDENTIFIER, TOK_ASYNC)) { + if (parseAsyncStatement(true)) { + return; + } + } + parseSimpleStatement(); } @@ -655,7 +661,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { while (!atAnyOfTokens(null, PyTokenTypes.DEDENT, PyTokenTypes.STATEMENT_BREAK, PyTokenTypes.COLON)) { myBuilder.advanceLexer(); } - boolean result = matchToken(PyTokenTypes.COLON); + boolean result = matchToken(PyTokenTypes.COLON); if (!result && atToken(PyTokenTypes.STATEMENT_BREAK)) { myBuilder.advanceLexer(); } @@ -821,23 +827,36 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { classMarker.done(PyElementTypes.CLASS_DECLARATION); } - private void parseAsyncStatement() { - assertCurrentToken(PyTokenTypes.ASYNC_KEYWORD); + private boolean parseAsyncStatement(boolean falseAsync) { + if (!falseAsync) { + assertCurrentToken(PyTokenTypes.ASYNC_KEYWORD); + } final PsiBuilder.Marker marker = myBuilder.mark(); - myBuilder.advanceLexer(); + + advanceAsync(falseAsync); + final IElementType token = myBuilder.getTokenType(); if (token == PyTokenTypes.DEF_KEYWORD) { getFunctionParser().parseFunctionDeclaration(marker, true); + return true; } else if (token == PyTokenTypes.WITH_KEYWORD) { parseWithStatement(marker); + return true; } else if (token == PyTokenTypes.FOR_KEYWORD) { parseForStatement(marker); + return true; } else { - marker.drop(); - myBuilder.error("'def' or 'with' or 'for' expected"); + if (falseAsync) { + marker.rollbackTo(); + } + else { + marker.drop(); + myBuilder.error("'def' or 'with' or 'for' expected"); + } + return false; } } diff --git a/python/testData/highlighting/async.py b/python/testData/highlighting/async.py index fbacffc57c36..de0afbc51a72 100644 --- a/python/testData/highlighting/async.py +++ b/python/testData/highlighting/async.py @@ -22,7 +22,7 @@ async = 1 async for x in xs: pass - async with xs: + async with xs: pass return async diff --git a/python/testData/psi/AsyncDefWithDecorator.py b/python/testData/psi/AsyncDefWithDecorator.py new file mode 100644 index 000000000000..5c7206a6598b --- /dev/null +++ b/python/testData/psi/AsyncDefWithDecorator.py @@ -0,0 +1,4 @@ +class AsyncIterator(AsyncIterable): + @abstractmethod + async def __anext__(self): + raise StopAsyncIteration diff --git a/python/testData/psi/AsyncFor.txt b/python/testData/psi/AsyncFor.txt index abadd01f0402..235e3ee2747a 100644 --- a/python/testData/psi/AsyncFor.txt +++ b/python/testData/psi/AsyncFor.txt @@ -30,13 +30,10 @@ PyFile:AsyncFor.py PyPassStatement PsiElement(Py:PASS_KEYWORD)('pass') PsiWhiteSpace('\n\n\n') - PyExpressionStatement - PyReferenceExpression: async - PsiElement(Py:IDENTIFIER)('async') - PsiErrorElement:End of statement expected - - PsiWhiteSpace(' ') PyForStatement + PsiErrorElement:'async' keyword is not expected here + PsiElement(Py:IDENTIFIER)('async') + PsiWhiteSpace(' ') PyForPart PsiElement(Py:FOR_KEYWORD)('for') PsiWhiteSpace(' ') diff --git a/python/testData/psi/AsyncWith.txt b/python/testData/psi/AsyncWith.txt index 0313005b086d..562634325892 100644 --- a/python/testData/psi/AsyncWith.txt +++ b/python/testData/psi/AsyncWith.txt @@ -25,13 +25,10 @@ PyFile:AsyncWith.py PyPassStatement PsiElement(Py:PASS_KEYWORD)('pass') PsiWhiteSpace('\n\n') - PyExpressionStatement - PyReferenceExpression: async - PsiElement(Py:IDENTIFIER)('async') - PsiErrorElement:End of statement expected - - PsiWhiteSpace(' ') PyWithStatement + PsiErrorElement:'async' keyword is not expected here + PsiElement(Py:IDENTIFIER)('async') + PsiWhiteSpace(' ') PsiElement(Py:WITH_KEYWORD)('with') PsiWhiteSpace(' ') PyWithItem diff --git a/python/testData/psi/ExecAsArgument.py b/python/testData/psi/ExecAsArgument.py new file mode 100644 index 000000000000..bf8f17d96da7 --- /dev/null +++ b/python/testData/psi/ExecAsArgument.py @@ -0,0 +1,6 @@ +def foo(): + baz(exec, 1) + + +def bar(): + pass diff --git a/python/testData/psi/PrintWithEndArgument.py b/python/testData/psi/PrintWithEndArgument.py new file mode 100644 index 000000000000..8fc37ced74e6 --- /dev/null +++ b/python/testData/psi/PrintWithEndArgument.py @@ -0,0 +1,6 @@ +def bar(): + print("header:", end=" ") + + +def foo(): + pass \ No newline at end of file diff --git a/python/testData/psi/StarParameter.py b/python/testData/psi/StarParameter.py new file mode 100644 index 000000000000..45e80e2ed902 --- /dev/null +++ b/python/testData/psi/StarParameter.py @@ -0,0 +1,6 @@ +def foo(arg1, *, kwarg1): + pass + + +def bar(): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyUnifiedStubsTest.kt b/python/testSrc/com/jetbrains/python/PyUnifiedStubsTest.kt index 0cfe8455d890..f8aabe96a9e5 100644 --- a/python/testSrc/com/jetbrains/python/PyUnifiedStubsTest.kt +++ b/python/testSrc/com/jetbrains/python/PyUnifiedStubsTest.kt @@ -15,30 +15,95 @@ */ package com.jetbrains.python -import com.intellij.psi.PsiManager +import com.intellij.psi.util.PsiTreeUtil import com.jetbrains.python.fixtures.PyTestCase import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PySingleStarParameter import junit.framework.TestCase /** * @author traff */ +/** + * Stubs should be language level independent. + * + * This test checks the cases when stubs can differ for different language levels. + * + */ class PyUnifiedStubsTest : PyTestCase() { override fun getTestDataPath(): String { return PythonTestUtil.getTestDataPath() } - fun testExecPy3() { + private fun doTest(function: (languageLevel: LanguageLevel, file: PyFile) -> Boolean) { for (level in LanguageLevel.SUPPORTED_LEVELS) { runWithLanguageLevel(level) { - val vf = myFixture.copyFileToProject("psi/${getTestName(false)}.py") - val file = PsiManager.getInstance(myFixture.project).findFile(vf) as PyFile + val file = getFile() - TestCase.assertEquals("Python $level", "exec", file.topLevelFunctions[0].name) + val shouldContainErrors = function(level, file) + + if (shouldContainErrors) { + TestCase.assertTrue(PsiTreeUtil.hasErrorElements(file)) + } } + tearDown() // re-init fixture to avoid using cached psi + setUp() } } + + private fun getFile(): PyFile { + val vf = myFixture.copyFileToProject("psi/${getTestName(false)}.py") + val file = myFixture.psiManager.findFile(vf) as PyFile + return file + } + + fun testExecPy3() { + doTest { level, file -> + TestCase.assertEquals("Python $level", "exec", file.topLevelFunctions[0].name) + level.isOutdatedPython2 + } + } + + fun testStarParameter() { + doTest { level, file -> + val functionStub = file.topLevelFunctions[0].stub + TestCase.assertNotNull("Function should contain star argument stub", + functionStub!!.childrenStubs[0].findChildStubByType( + PyElementTypes.SINGLE_STAR_PARAMETER)) + level.isOutdatedPython2 + } + } + + fun testAnnotations() { + doTest { level, file -> + val functionStub = file.topLevelFunctions[0].stub + TestCase.assertNotNull("Function should contain annotation stub", functionStub!!.findChildStubByType(PyElementTypes.ANNOTATION)) + level.isOutdatedPython2 + } + } + + fun testPrintWithEndArgument() { + doTest { level, file -> + TestCase.assertEquals("'end' argument shouldn't break tuple parsing for Python 2", 2, file.topLevelFunctions.size) + level.isOutdatedPython2 + } + } + + fun testExecAsArgument() { + doTest { level, file -> + TestCase.assertEquals("'exec' used as an argument shouldn't break parser for Python 2", 2, file.topLevelFunctions.size) + level.isOutdatedPython2 + } + } + + fun testAsyncDefWithDecorator() { + doTest { level, file -> + TestCase.assertEquals("Async keyword shouldn't break function parsing for Python <3.5", 1, file.topLevelClasses[0].methods.size) + level.isOlderThan(LanguageLevel.PYTHON35) + } + } + } \ No newline at end of file diff --git a/python/tools/src/com/jetbrains/python/tools/PyPrebuiltIndicesGenerator.kt b/python/tools/src/com/jetbrains/python/tools/PyPrebuiltIndicesGenerator.kt index 11dc26543c2d..06f49a3bf54c 100644 --- a/python/tools/src/com/jetbrains/python/tools/PyPrebuiltIndicesGenerator.kt +++ b/python/tools/src/com/jetbrains/python/tools/PyPrebuiltIndicesGenerator.kt @@ -15,6 +15,7 @@ import com.intellij.util.ui.UIUtil import com.jetbrains.python.psi.impl.stubs.PyPrebuiltStubsProvider import org.jetbrains.index.id.IdIndexGenerator import java.io.File +import java.util.* fun main(args: Array) { buildIndices(args[0], "${args[1]}/${PyPrebuiltStubsProvider.NAME}") diff --git a/tools/index-tools/src/org/jetbrains/index/stubs/StubsGenerator.kt b/tools/index-tools/src/org/jetbrains/index/stubs/StubsGenerator.kt index 5bfc4335b36e..db7659ea7539 100644 --- a/tools/index-tools/src/org/jetbrains/index/stubs/StubsGenerator.kt +++ b/tools/index-tools/src/org/jetbrains/index/stubs/StubsGenerator.kt @@ -36,7 +36,6 @@ import com.intellij.util.indexing.FileContentImpl import com.intellij.util.io.PersistentHashMap import junit.framework.TestCase import org.jetbrains.index.IndexGenerator -import java.io.ByteArrayInputStream import java.io.File import java.util.* @@ -226,21 +225,24 @@ abstract class LanguageLevelAwareStubsGenerator(stubsVersion: String, stubsSt applyLanguageLevel(languageLevel) + // create new FileContentImpl, because it caches stub in user data + val content = FileContentImpl(fileContent.file, fileContent.content) - val stub = super.buildStubForFile(fileContent, serializationManager) + val stub = super.buildStubForFile(content, serializationManager) val bytes = BufferExposingByteArrayOutputStream() serializationManager.serialize(stub, bytes) - if (prevLanguageLevelBytes != null) { - if (!Arrays.equals(bytes.toByteArray(), prevLanguageLevelBytes)) { - val stub2 = serializationManager.deserialize(ByteArrayInputStream(prevLanguageLevelBytes)) - val msg = "Stubs are different for ${fileContent.file.path} between Python versions $prevLanguageLevel and $languageLevel.\n" - TestCase.assertEquals(msg, DebugUtil.stubTreeToString(stub), DebugUtil.stubTreeToString(stub2)) + if (prevStub != null) { + try { + check(stub, prevStub) + } catch (e: AssertionError) { + val msg = "Stubs are different for ${content.file.path} between Python versions $prevLanguageLevel and $languageLevel.\n" + TestCase.assertEquals(msg, DebugUtil.stubTreeToString(stub), DebugUtil.stubTreeToString(prevStub)) TestCase.fail(msg + "But DebugUtil.stubTreeToString values of stubs are unfortunately equal.") } } - prevLanguageLevelBytes = bytes.toByteArray() + prevLanguageLevel = languageLevel prevStub = stub } @@ -251,3 +253,16 @@ abstract class LanguageLevelAwareStubsGenerator(stubsVersion: String, stubsSt } } +private fun check(stub: Stub, stub2: Stub) { + TestCase.assertEquals(stub.stubType, stub2.stubType) + val stubs = stub.childrenStubs + val stubs2 = stub2.childrenStubs + TestCase.assertEquals(stubs.size, stubs2.size) + var i = 0 + val len = stubs.size + while (i < len) { + check(stubs[i], stubs2[i]) + ++i + } +} +