package org.elixir_lang.intellij_elixir; import com.ericsson.otp.erlang.*; import com.intellij.psi.PsiFile; import org.apache.commons.lang.NotImplementedException; import org.elixir_lang.GenericServer; import org.elixir_lang.IntellijElixir; import org.elixir_lang.psi.impl.ElixirPsiImplUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.junit.ComparisonFailure; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import static org.apache.commons.lang.CharUtils.isAsciiPrintable; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.*; /** * Created by luke.imhoff on 12/31/14. */ public class Quoter { public static final OtpErlangAtom CODE_KEY = new OtpErlangAtom("code"); public static final OtpErlangAtom QUOTED_KEY = new OtpErlangAtom("quoted"); /* remote name is Elixir.IntellijElixir.Quoter because all aliases in Elixir look like atoms prefixed with with Elixir. from erlang's perspective. */ public static final String REMOTE_NAME = "Elixir.IntellijElixir.Quoter"; public static final int TIMEOUT_IN_MILLISECONDS = 1000; public static void assertMessageReceived(OtpErlangObject message) { assertNotNull( "did not receive message from " + REMOTE_NAME + "@" + IntellijElixir.REMOTE_NODE + ". Make sure it is running", message ); } public static void assertError(PsiFile file) { final String text = file.getText(); try { OtpErlangTuple quotedMessage = Quoter.quote(text); Quoter.assertMessageReceived(quotedMessage); OtpErlangAtom status = (OtpErlangAtom) quotedMessage.elementAt(0); String statusString = status.atomValue(); assertEquals(statusString, "error"); } catch (IOException e) { throw new RuntimeException(e); } catch (OtpErlangDecodeException e) { throw new RuntimeException(e); } catch (OtpErlangExit e) { throw new RuntimeException(e); } } public static void assertExit(PsiFile file) { final String text = file.getText(); Object exception = null; try { Quoter.quote(text); } catch (IOException ioExeption) { exception = ioExeption; } catch (OtpErlangDecodeException otpErlangDecodeException) { exception = otpErlangDecodeException; } catch (OtpErlangExit otpErlangExit) { exception = otpErlangExit; } assertThat(exception, instanceOf(OtpErlangExit.class)); } public static void assertQuotedCorrectly(PsiFile file) { final String text = file.getText(); try { OtpErlangTuple quotedMessage = Quoter.quote(text); Quoter.assertMessageReceived(quotedMessage); OtpErlangAtom status = (OtpErlangAtom) quotedMessage.elementAt(0); String statusString = status.atomValue(); OtpErlangObject expectedQuoted = quotedMessage.elementAt(1); if (statusString.equals("ok")) { OtpErlangObject actualQuoted = ElixirPsiImplUtil.quote(file); assertQuotedCorrectly(expectedQuoted, actualQuoted); } else if (statusString.equals("error")) { OtpErlangTuple error = (OtpErlangTuple) expectedQuoted; OtpErlangLong line = (OtpErlangLong) error.elementAt(0); OtpErlangBinary messageBinary = (OtpErlangBinary) error.elementAt(1); String message = javaString(messageBinary); OtpErlangBinary tokenBinary = (OtpErlangBinary) error.elementAt(2); String token = javaString(tokenBinary); throw new AssertionError( "intellij_elixir returned \"" + message + "\" on line " + line + " due to " + token + ", use assertQuotesAroundError if error is expect in Elixir natively, " + "but not in intellij-elixir plugin" ); } } catch (IOException e) { throw new RuntimeException(e); } catch (OtpErlangDecodeException e) { throw new RuntimeException(e); } catch (OtpErlangExit e) { throw new RuntimeException(e); } } public static void assertQuotedCorrectly(@NotNull OtpErlangObject expectedQuoted, @NotNull OtpErlangObject actualQuoted) { if(!expectedQuoted.equals(actualQuoted)) { throw new ComparisonFailure(null, toString(expectedQuoted), toString(actualQuoted)); } } @Contract(pure = true) @NotNull public static String code(@NotNull OtpErlangMap quotedMessageMap) { OtpErlangBinary actualElixirString = (OtpErlangBinary) quotedMessageMap.get(CODE_KEY); byte[] actualBytes = actualElixirString.binaryValue(); return new String(actualBytes, Charset.forName("UTF-8")); } @NotNull public static OtpErlangObject elixirCharList(@NotNull final List<Integer> codePointList) { OtpErlangList elixirCodePointList = elixirCodePointList(codePointList); return elixirCharList(elixirCodePointList); } /* * Erlang will automatically stringify a list that is just a list of LATIN-1 printable code * points. * OtpErlangString and OtpErlangList are not equal when they have the same content, so to check against * Elixir.Code.string_to_quoted, this code must determine if Erlang would return an OtpErlangString instead * of OtpErlangList and do the same. */ @NotNull public static OtpErlangObject elixirCharList(@NotNull final OtpErlangList erlangList) { OtpErlangObject charList; /* JInterface will return an OtpErlangString in some case and an OtpErlangList in other. Right now, I'm assuming it works similar to the printing in `iex` and is based on whether the codePoint is printable, but ASCII printable instead of Unicode printable since Erlang is ASCII/LATIN-1 based */ if (isErlangPrintable(erlangList)) { try { charList = new OtpErlangString(erlangList); } catch (OtpErlangException e) { throw new NotImplementedException(e); } } else { charList = erlangList; } return charList; } @NotNull public static OtpErlangList elixirCodePointList(@NotNull final List<Integer> codePointList) { OtpErlangLong[] erlangCodePoints = new OtpErlangLong[codePointList.size()]; int i = 0; for (int codePoint : codePointList) { erlangCodePoints[i++] = new OtpErlangLong(codePoint); } return new OtpErlangList(erlangCodePoints); } public static boolean isErlangPrintable(@NotNull final OtpErlangList erlangList) { boolean isErlangPrintable = true; for (OtpErlangObject erlangObject : erlangList) { if (erlangObject instanceof OtpErlangLong) { OtpErlangLong erlangLong = (OtpErlangLong) erlangObject; final int codePoint; try { codePoint = erlangLong.intValue(); } catch (OtpErlangRangeException e) { isErlangPrintable = false; break; } if (!isErlangPrintable(codePoint)) { isErlangPrintable = false; break; } } else { isErlangPrintable = false; break; } } if (erlangList.arity() == 0) { isErlangPrintable = false; } return isErlangPrintable; } @NotNull public static OtpErlangBinary elixirString(@NotNull final List<Integer> codePointList) { StringBuilder stringAccumulator = new StringBuilder(); for (int codePoint : codePointList) { stringAccumulator.appendCodePoint(codePoint); } return elixirString(stringAccumulator.toString()); } @NotNull public static OtpErlangBinary elixirString(@NotNull String javaString) { final byte[] bytes = javaString.getBytes(Charset.forName("UTF-8")); return new OtpErlangBinary(bytes); } @Contract(pure = true) public static boolean isErlangPrintable(int codePoint) { return (codePoint >= 0 && codePoint <= 255); } @NotNull public static String javaString(@NotNull OtpErlangBinary elixirString) { byte[] bytes = elixirString.binaryValue(); return new String(bytes, Charset.forName("UTF-8")); } public static OtpErlangTuple quote(@NotNull String code) throws IOException, OtpErlangExit, OtpErlangDecodeException { final OtpNode otpNode = IntellijElixir.getLocalNode(); final OtpMbox otpMbox = otpNode.createMbox(); OtpErlangObject request = elixirString(code); return (OtpErlangTuple) GenericServer.call( otpMbox, otpNode, REMOTE_NAME, IntellijElixir.REMOTE_NODE, request, TIMEOUT_IN_MILLISECONDS ); } @NotNull public static String toString(@NotNull OtpErlangBitstr quoted) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append('"'); for (byte element : quoted.binaryValue()) { if (element == 0x0A) { stringBuilder.append("\\n"); } else if (isAsciiPrintable((char) element)) { stringBuilder.append((char) element); } else { stringBuilder.append(String.format("\\x%02X", element)); } } stringBuilder.append('"'); return stringBuilder.toString(); } @NotNull public static String toString(@NotNull OtpErlangList quoted) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("["); for (int i = 0; i< quoted.arity(); i++) { if (i > 0) { stringBuilder.append(","); } stringBuilder.append(toString(quoted.elementAt(i))); } stringBuilder.append("]"); return stringBuilder.toString(); } @NotNull public static String toString(@NotNull OtpErlangObject quoted) { String string; if (quoted instanceof OtpErlangBoolean || quoted instanceof OtpErlangAtom || quoted instanceof OtpErlangByte || quoted instanceof OtpErlangChar || quoted instanceof OtpErlangFloat || quoted instanceof OtpErlangDouble || quoted instanceof OtpErlangExternalFun || quoted instanceof OtpErlangFun || quoted instanceof OtpErlangInt || quoted instanceof OtpErlangLong || quoted instanceof OtpErlangMap || quoted instanceof OtpErlangPid || quoted instanceof OtpErlangString) { string = quoted.toString(); } else if (quoted instanceof OtpErlangBitstr) { string = toString((OtpErlangBitstr) quoted); } else if (quoted instanceof OtpErlangList) { string = toString((OtpErlangList) quoted); } else if (quoted instanceof OtpErlangTuple) { string = toString((OtpErlangTuple) quoted); } else { throw new IllegalArgumentException("Don't know how to convert " + quoted.getClass() + " to string"); } return string; } @NotNull public static String toString(@NotNull OtpErlangTuple quoted) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("{"); for (int i = 0; i < quoted.arity(); i++) { if (i > 0) { stringBuilder.append(","); } stringBuilder.append(toString(quoted.elementAt(i))); } stringBuilder.append("}"); return stringBuilder.toString(); } }