diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90f430c..4e75236 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,4 +30,4 @@ jobs: SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew publish -PossrhToken="${SONATYPE_USERNAME}" -PossrhTokenPassword="${SONATYPE_PASSWORD}" -PsigningKey="${SIGNING_KEY}" -PsigningPassword="${SIGNING_PASSWORD}" -PideaVersion=IC-2020.3 -PlibraryVersion=$GITHUB_REF_NAME + run: ./gradlew publish -PossrhToken="${SONATYPE_USERNAME}" -PossrhTokenPassword="${SONATYPE_PASSWORD}" -PsigningKey="${SIGNING_KEY}" -PsigningPassword="${SIGNING_PASSWORD}" -PideaVersion=IC-2020.3 -PlibraryVersion=$GITHUB_REF_NAME \ No newline at end of file diff --git a/src/test/antlr/issue2/Issue2.g4 b/src/test/antlr/org/antlr/intellij/adaptor/test/testcases/Issue2.g4 similarity index 69% rename from src/test/antlr/issue2/Issue2.g4 rename to src/test/antlr/org/antlr/intellij/adaptor/test/testcases/Issue2.g4 index 5a69779..219b37c 100644 --- a/src/test/antlr/issue2/Issue2.g4 +++ b/src/test/antlr/org/antlr/intellij/adaptor/test/testcases/Issue2.g4 @@ -1,7 +1,7 @@ grammar Issue2; @header { -package org.antlr.intellij.adaptor.issue2; +package org.antlr.intellij.adaptor.test.testcases; } block @@ -15,4 +15,4 @@ usesList ; ID: [a-zA-Z]+; -WS: [\t\r\n ]+ -> skip; +WS: [\t\r\n ]+ -> skip; \ No newline at end of file diff --git a/src/test/java/issue2/Issue2FileType.java b/src/test/java/issue2/Issue2FileType.java deleted file mode 100644 index b22238e..0000000 --- a/src/test/java/issue2/Issue2FileType.java +++ /dev/null @@ -1,40 +0,0 @@ -package issue2; - -import com.intellij.openapi.fileTypes.LanguageFileType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; - -class Issue2FileType extends LanguageFileType { - - static Issue2FileType INSTANCE = new Issue2FileType(); - - private Issue2FileType() { - super(Issue2Language.INSTANCE); - } - - @NotNull - @Override - public String getName() { - return "Issue 2"; - } - - @NotNull - @Override - public String getDescription() { - return "Issue 2"; - } - - @NotNull - @Override - public String getDefaultExtension() { - return "ext"; - } - - @Nullable - @Override - public Icon getIcon() { - return null; - } -} diff --git a/src/test/java/issue2/Issue2Language.java b/src/test/java/issue2/Issue2Language.java deleted file mode 100644 index 6c0d939..0000000 --- a/src/test/java/issue2/Issue2Language.java +++ /dev/null @@ -1,12 +0,0 @@ -package issue2; - -import com.intellij.lang.Language; - -class Issue2Language extends Language { - - static Issue2Language INSTANCE = new Issue2Language(); - - private Issue2Language() { - super("Issue2"); - } -} diff --git a/src/test/java/issue2/Issue2ParserDefinition.java b/src/test/java/issue2/Issue2ParserDefinition.java deleted file mode 100644 index a9cd827..0000000 --- a/src/test/java/issue2/Issue2ParserDefinition.java +++ /dev/null @@ -1,85 +0,0 @@ -package issue2; - -import com.intellij.extapi.psi.PsiFileBase; -import com.intellij.lang.ASTNode; -import com.intellij.lang.ParserDefinition; -import com.intellij.lang.PsiParser; -import com.intellij.lexer.Lexer; -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.project.Project; -import com.intellij.psi.FileViewProvider; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.tree.IElementType; -import com.intellij.psi.tree.IFileElementType; -import com.intellij.psi.tree.TokenSet; -import org.antlr.intellij.adaptor.issue2.Issue2Lexer; -import org.antlr.intellij.adaptor.issue2.Issue2Parser; -import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; -import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; -import org.antlr.intellij.adaptor.parser.ANTLRParserAdaptor; -import org.antlr.intellij.adaptor.psi.ANTLRPsiNode; -import org.antlr.v4.runtime.Parser; -import org.antlr.v4.runtime.tree.ParseTree; -import org.jetbrains.annotations.NotNull; - -public class Issue2ParserDefinition implements ParserDefinition { - - public Issue2ParserDefinition() { - PSIElementTypeFactory.defineLanguageIElementTypes( - Issue2Language.INSTANCE, - Issue2Lexer.VOCABULARY, - Issue2Parser.ruleNames - ); - } - - @NotNull - @Override - public Lexer createLexer(Project project) { - return new ANTLRLexerAdaptor(Issue2Language.INSTANCE, new Issue2Lexer(null)); - } - - @Override - public PsiParser createParser(Project project) { - return new ANTLRParserAdaptor(Issue2Language.INSTANCE, new Issue2Parser(null)) { - @Override - protected ParseTree parse(Parser parser, IElementType root) { - return ((Issue2Parser) parser).block(); - } - }; - } - - @Override - public IFileElementType getFileNodeType() { - return new IFileElementType(Issue2Language.INSTANCE); - } - - @NotNull - @Override - public TokenSet getCommentTokens() { - return TokenSet.EMPTY; - } - - @NotNull - @Override - public TokenSet getStringLiteralElements() { - return TokenSet.EMPTY; - } - - @NotNull - @Override - public PsiElement createElement(ASTNode node) { - return new ANTLRPsiNode(node); - } - - @Override - public PsiFile createFile(FileViewProvider viewProvider) { - return new PsiFileBase(viewProvider, Issue2Language.INSTANCE) { - @NotNull - @Override - public FileType getFileType() { - return Issue2FileType.INSTANCE; - } - }; - } -} diff --git a/src/test/java/issue2/Issue2ParserTest.java b/src/test/java/issue2/Issue2ParserTest.java deleted file mode 100644 index 5d33890..0000000 --- a/src/test/java/issue2/Issue2ParserTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package issue2; - -import com.intellij.testFramework.ParsingTestCase; - -public class Issue2ParserTest extends ParsingTestCase { - - public Issue2ParserTest() { - super("issue2", "ext", true, new Issue2ParserDefinition()); - } - - public void testIssue2() { - doTest(true); - } - - @Override - protected String getTestDataPath() { - return "src/test/resources/testData"; - } -} diff --git a/src/test/java/org/antlr/intellij/adaptor/test/AntlrTestCase.java b/src/test/java/org/antlr/intellij/adaptor/test/AntlrTestCase.java new file mode 100644 index 0000000..3695ba4 --- /dev/null +++ b/src/test/java/org/antlr/intellij/adaptor/test/AntlrTestCase.java @@ -0,0 +1,45 @@ +package org.antlr.intellij.adaptor.test; + +import com.intellij.lang.Language; +import com.intellij.testFramework.ParsingTestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; + +@RunWith(Parameterized.class) +public class AntlrTestCase extends ParsingTestCase{ + + private final String name; + + @Parameterized.Parameters(name = "{index}: {2} for {0}/{1}") + public static Iterable parameters(){ + return Arrays.asList((Object[]) new Object[][]{ + { "Issue2", "block", "issue2" }, + { "Issue2", "block", "straightforward" } + }); + } + + public AntlrTestCase(String lang, String root, String name){ + this(lang, root, name, TestLanguage.synthesizeTestLanguage(lang)); + } + + private AntlrTestCase(String lang, String root, String name, Language language){ + super(lang, lang, TestParserDefinition.byName(lang, root, language, new TestFileType(lang, language))); + this.name = name; + } + + public String getName(){ + return name; + } + + protected String getTestDataPath(){ + return "src/test/resources/testData"; + } + + @Test + public void test(){ + doTest(true); + } +} \ No newline at end of file diff --git a/src/test/java/org/antlr/intellij/adaptor/test/TestFileType.java b/src/test/java/org/antlr/intellij/adaptor/test/TestFileType.java new file mode 100644 index 0000000..6c7c4ea --- /dev/null +++ b/src/test/java/org/antlr/intellij/adaptor/test/TestFileType.java @@ -0,0 +1,35 @@ +package org.antlr.intellij.adaptor.test; + +import com.intellij.lang.Language; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.NlsContexts; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class TestFileType extends LanguageFileType{ + + private final String name; + + public TestFileType(String name, Language language){ + super(language); + this.name = name; + } + + public @NonNls @NotNull String getName(){ + return name; + } + + public @NlsContexts.Label @NotNull String getDescription(){ + return name; + } + + public @NotNull String getDefaultExtension(){ + return name; + } + + public Icon getIcon(){ + return null; + } +} \ No newline at end of file diff --git a/src/test/java/org/antlr/intellij/adaptor/test/TestLanguage.java b/src/test/java/org/antlr/intellij/adaptor/test/TestLanguage.java new file mode 100644 index 0000000..2cb8433 --- /dev/null +++ b/src/test/java/org/antlr/intellij/adaptor/test/TestLanguage.java @@ -0,0 +1,69 @@ +package org.antlr.intellij.adaptor.test; + +import com.intellij.lang.Language; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.org.objectweb.asm.ClassWriter; +import org.jetbrains.org.objectweb.asm.MethodVisitor; +import org.jetbrains.org.objectweb.asm.Opcodes; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +/** + * A generic class for testing languages. Note that multiple instances of a language class cannot be used + * and will be rejected by the IntelliJ runtime; instead, a subclass must be generated for each test case, + * hence the abstract label. + */ +public abstract class TestLanguage extends Language{ + + protected TestLanguage(@NonNls @NotNull String lang){ + super(lang); + } + + private static final Map languageCache = new HashMap<>(); + + /** + * Generates a new subclass of {@linkplain TestLanguage}, and returns the canonical instance. + */ + public static TestLanguage synthesizeTestLanguage(@NotNull String lang){ + if(languageCache.containsKey(lang)) + return languageCache.get(lang); + + // Create a subclass of TestLanguage... + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + long l = System.currentTimeMillis(); + writer.visit( + Opcodes.V1_8, + Opcodes.ACC_SUPER | Opcodes.ACC_PUBLIC, + "org/antlr/intellij/adaptor/test/$Lang" + lang, + "", + "org/antlr/intellij/adaptor/test/TestLanguage", + new String[0] + ); + + // ...with one constructor that takes 0 parameters and has no type variables... + MethodVisitor ctor = writer.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", "", new String[0]); + ctor.visitCode(); + // ...that invokes the super constructor with itself and the language name... + ctor.visitVarInsn(Opcodes.ALOAD, 0); + ctor.visitLdcInsn(lang); + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/antlr/intellij/adaptor/test/TestLanguage", "", "(Ljava/lang/String;)V", false); + ctor.visitInsn(Opcodes.RETURN); + ctor.visitMaxs(0, 0); + ctor.visitEnd(); + writer.visitEnd(); + + try{ + // ...then define this class and return one instance. + Class cls = MethodHandles.lookup().defineClass(writer.toByteArray()); + TestLanguage language = (TestLanguage)cls.getConstructor().newInstance(); + languageCache.put(lang, language); + return language; + }catch(IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e){ + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/antlr/intellij/adaptor/test/TestParserDefinition.java b/src/test/java/org/antlr/intellij/adaptor/test/TestParserDefinition.java new file mode 100644 index 0000000..6d61d11 --- /dev/null +++ b/src/test/java/org/antlr/intellij/adaptor/test/TestParserDefinition.java @@ -0,0 +1,116 @@ +package org.antlr.intellij.adaptor.test; + +import com.intellij.extapi.psi.PsiFileBase; +import com.intellij.lang.ASTNode; +import com.intellij.lang.Language; +import com.intellij.lang.ParserDefinition; +import com.intellij.lang.PsiParser; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.tree.IFileElementType; +import com.intellij.psi.tree.TokenSet; +import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; +import org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory; +import org.antlr.intellij.adaptor.parser.ANTLRParserAdaptor; +import org.antlr.intellij.adaptor.psi.ANTLRPsiNode; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.tree.ParseTree; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class TestParserDefinition implements ParserDefinition{ + + private final Class parserClass; + private final Class lexerClass; + private final Method rootNodeParser; + private final Language language; + private final FileType fileType; + + public TestParserDefinition(Class parserClass, Class lexerClass, Method parser, Language language, FileType fileType){ + this.parserClass = parserClass; + this.lexerClass = lexerClass; + rootNodeParser = parser; + this.language = language; + this.fileType = fileType; + + try{ + PSIElementTypeFactory.defineLanguageIElementTypes( + language, + (Vocabulary)lexerClass.getDeclaredField("VOCABULARY").get(null), + (String[])parserClass.getDeclaredField("ruleNames").get(null) + ); + }catch(IllegalAccessException | NoSuchFieldException e){ + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public static TestParserDefinition byName(String name, String root, Language language, FileType fileType){ + try{ + Class parserClass = Class.forName("org.antlr.intellij.adaptor.test.testcases." + name + "Parser"); + Class lexerClass = Class.forName("org.antlr.intellij.adaptor.test.testcases." + name + "Lexer"); + if(!Parser.class.isAssignableFrom(parserClass) || !Lexer.class.isAssignableFrom(lexerClass)) + throw new IllegalArgumentException("Given parser/lexer class doesn't exist, or is invalid"); + Method rootNodeParser = parserClass.getDeclaredMethod(root); + if(!ParseTree.class.isAssignableFrom(rootNodeParser.getReturnType())) + throw new IllegalArgumentException("Given root node method is not a parser"); + return new TestParserDefinition((Class)parserClass, (Class)lexerClass, rootNodeParser, language, fileType); + }catch(ReflectiveOperationException e){ + throw new RuntimeException(e); + } + } + + public @NotNull com.intellij.lexer.Lexer createLexer(Project project){ + try{ + return new ANTLRLexerAdaptor(language, lexerClass.getConstructor(CharStream.class).newInstance((Object)null)); + }catch(InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e){ + throw new RuntimeException(e); + } + } + + public @NotNull PsiParser createParser(Project project){ + try{ + return new ANTLRParserAdaptor(language, parserClass.getConstructor(TokenStream.class).newInstance((Object)null)){ + protected ParseTree parse(org.antlr.v4.runtime.Parser parser, IElementType root){ + try{ + return (ParseTree)rootNodeParser.invoke(parser); + }catch(IllegalAccessException | InvocationTargetException e){ + throw new RuntimeException(e); + } + } + }; + }catch(NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e){ + throw new RuntimeException(e); + } + } + + public @NotNull IFileElementType getFileNodeType(){ + return new IFileElementType(language); + } + + public @NotNull TokenSet getCommentTokens(){ + return TokenSet.EMPTY; + } + + public @NotNull TokenSet getStringLiteralElements(){ + return TokenSet.EMPTY; + } + + public @NotNull PsiElement createElement(ASTNode node){ + return new ANTLRPsiNode(node); + } + + public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider){ + return new PsiFileBase(viewProvider, language){ + public @NotNull FileType getFileType(){ + return fileType; + } + }; + } +} \ No newline at end of file diff --git a/src/test/resources/testData/issue2/issue2.ext b/src/test/resources/testData/Issue2/issue2.Issue2 similarity index 100% rename from src/test/resources/testData/issue2/issue2.ext rename to src/test/resources/testData/Issue2/issue2.Issue2 diff --git a/src/test/resources/testData/issue2/issue2.txt b/src/test/resources/testData/Issue2/issue2.txt similarity index 100% rename from src/test/resources/testData/issue2/issue2.txt rename to src/test/resources/testData/Issue2/issue2.txt diff --git a/src/test/resources/testData/Issue2/straightforward.Issue2 b/src/test/resources/testData/Issue2/straightforward.Issue2 new file mode 100644 index 0000000..2656fe3 --- /dev/null +++ b/src/test/resources/testData/Issue2/straightforward.Issue2 @@ -0,0 +1,3 @@ +start X; + uses Nothing; +end X; \ No newline at end of file diff --git a/src/test/resources/testData/Issue2/straightforward.txt b/src/test/resources/testData/Issue2/straightforward.txt new file mode 100644 index 0000000..c83ae5f --- /dev/null +++ b/src/test/resources/testData/Issue2/straightforward.txt @@ -0,0 +1,12 @@ +FILE + ANTLRPsiNode(block) + PsiElement('start')('start ') + PsiElement(ID)('X') + PsiElement(';')(';\n\t') + ANTLRPsiNode(usesList) + PsiElement('uses')('uses ') + PsiElement(ID)('Nothing') + PsiElement(';')(';\n') + PsiElement('end')('end ') + PsiElement(ID)('X') + PsiElement(';')(';') \ No newline at end of file