package com.jetbrains.lang.dart.ide.runner; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Locale; public class DartPositionInfo { public enum Type { FILE, DART, PACKAGE; @Nullable public static Type getType(final String type) { if ("file".equals(type)) return FILE; if ("dart".equals(type)) return DART; if ("package".equals(type)) return PACKAGE; return null; } } public final @NotNull Type type; public final @NotNull String path; public final int highlightingStartIndex; public final int highlightingEndIndex; public final int line; public final int column; public DartPositionInfo(final @NotNull Type type, final @NotNull String path, final int highlightingStartIndex, final int highlightingEndIndex, final int line, final int column) { this.type = type; this.path = path; this.highlightingStartIndex = highlightingStartIndex; this.highlightingEndIndex = highlightingEndIndex; this.line = line; this.column = column; } /* #0 min (dart:math:70) #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:42) #1 SplayTreeMap.addAll (dart:collection/splay_tree.dart:373) #1 Sphere.copyFrom (package:vector_math/src/vector_math/sphere.dart:46:23) #1 StringBuffer.writeAll (dart:core/string_buffer.dart:41) #2 main (file:///C:/dart/DartSample2/web/Bar.dart:4:28) #3 _startIsolate.isolateStartHandler (dart:isolate-patch/isolate_patch.dart:190) #4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:93) 'package:DartSample2/mylib.dart': error: line 7 pos 1: 'myLibPart' is already defined 'file:///C:/dart/DartSample2/bin/file2.dart': error: line 3 pos 1: library handler failed WHATEVER_NOT_ENDING_WITH_PATH_SYMBOL package:DartSample2/mylib.dart WHATEVER_ELSE_NOT_STARTING_FROM_PATH_SYMBOL WHATEVER_NOT_ENDING_WITH_PATH_SYMBOL dart:core/string_buffer.dart WHATEVER_ELSE_NOT_STARTING_FROM_PATH_SYMBOL inside WHATEVER_ELSE_STARTING_NOT_FROM_PATH_SYMBOL look for ':4:28' at the beginning or for 'line 3 pos 1' at any place */ @Nullable public static DartPositionInfo parsePositionInfo(final @NotNull String text) { Couple<Integer> pathStartAndEnd = parseUrlStartAndEnd(text, "package:"); if (pathStartAndEnd == null) pathStartAndEnd = parseUrlStartAndEnd(text, "dart:"); if (pathStartAndEnd == null) pathStartAndEnd = parseUrlStartAndEnd(text, "file:"); if (pathStartAndEnd == null) pathStartAndEnd = parseDartLibUrlStartAndEnd(text); if (pathStartAndEnd == null) return null; final Integer urlStartIndex = pathStartAndEnd.first; final Integer urlEndIndex = pathStartAndEnd.second; final String url = text.substring(urlStartIndex, urlEndIndex); final String tail = text.length() == urlEndIndex ? "" : text.substring(urlEndIndex).trim(); final Couple<Integer> lineAndColumn = parseLineAndColumn(tail); final int colonIndexInUrl = url.indexOf(':'); assert colonIndexInUrl > 0 : text; final Type type = Type.getType(url.substring(0, colonIndexInUrl)); assert type != null : text; final int pathStartIndexInUrl = type == Type.FILE ? colonIndexInUrl + 1 + getPathStartIndex(url.substring(colonIndexInUrl + 1)) : colonIndexInUrl + 1; final String path = url.substring(pathStartIndexInUrl, url.length()); final int line = lineAndColumn == null ? -1 : lineAndColumn.first >= 0 ? lineAndColumn.first - 1 : lineAndColumn.first; final int column = lineAndColumn == null ? -1 : lineAndColumn.second >= 0 ? lineAndColumn.second - 1 : lineAndColumn.second; return new DartPositionInfo(type, FileUtil.toSystemIndependentName(path), urlStartIndex, urlEndIndex, line, column); } @Nullable public static Couple<Integer> parseLineAndColumn(@NotNull final String text) { Couple<Integer> result = parseLineAndColumnInColonFormat(text); return result != null ? result : parseLineAndColumnInTextFormat(text); } // WHATEVER_NOT_ENDING_WITH_PATH_SYMBOL PREFIX PATH_ENDING_WITH_DOT_DART WHATEVER_ELSE_NOT_STARTING_FROM_PATH_SYMBOL // Example: 'package:DartSample2/mylib.dart': error: line 7 pos 1: 'myLibPart' is already defined @Nullable private static Couple<Integer> parseUrlStartAndEnd(final String text, final String prefix) { final int pathStartIndex = text.indexOf(prefix); if (pathStartIndex < 0 || pathStartIndex > 0 && !isCharAllowedBeforePath(text.charAt(pathStartIndex - 1))) { return null; } final String lowercased = text.toLowerCase(Locale.US); int dotDartIndex = pathStartIndex; int pathEndIndex; do { dotDartIndex = lowercased.indexOf(".dart", dotDartIndex + 1); pathEndIndex = dotDartIndex + ".dart".length(); } while (dotDartIndex > 0 && text.length() > pathEndIndex && !isCharAllowedAfterPath(text.charAt(pathEndIndex))); if (dotDartIndex <= pathStartIndex || text.length() > pathEndIndex && !isCharAllowedAfterPath(text.charAt(pathEndIndex))) { return null; } return Couple.of(pathStartIndex, pathEndIndex); } // #0 min (dart:math:70) @Nullable private static Couple<Integer> parseDartLibUrlStartAndEnd(final String text) { final int pathStartIndex = text.indexOf("dart:"); if (pathStartIndex < 0 || pathStartIndex > 0 && !isCharAllowedBeforePath(text.charAt(pathStartIndex - 1))) { return null; } final int libNameStartIndex = pathStartIndex + "dart:".length(); int index = libNameStartIndex; while (text.length() > index && ('_' == text.charAt(index) || Character.isLetter(text.charAt(index)))) index++; if (index == libNameStartIndex) return null; return Couple.of(pathStartIndex, index); } private static boolean isCharAllowedBeforePath(final char ch) { return !Character.isLetterOrDigit(ch); // allow spaces, punctuation, parens, brackets, braces, e.g. almost everything } private static boolean isCharAllowedAfterPath(final char ch) { // heuristics for paths like foo/angular.dart/path or foo/angular.dart.examples/path return !Character.isLetterOrDigit(ch) && ch != '/' && ch != '.' && ch != '_'; } @Nullable private static Couple<Integer> parseLineAndColumnInColonFormat(final @NotNull String text) { // "12 whatever, ":12 whatever", "12:34 whatever" or ":12:34 whatever" final Pair<Integer, String> lineAndRemainingText = parseNextIntSkippingColon(text); if (lineAndRemainingText == null) return null; final Pair<Integer, String> colonAndRemainingText = parseNextIntSkippingColon(lineAndRemainingText.second.trim()); if (colonAndRemainingText == null) { return Couple.of(lineAndRemainingText.first, -1); } else { return Couple.of(lineAndRemainingText.first, colonAndRemainingText.first); } } @Nullable private static Couple<Integer> parseLineAndColumnInTextFormat(final @NotNull String text) { // "whatever line 12 pos 34 whatever" int index = text.indexOf("line "); if (index == -1) return null; index += "line ".length(); final Pair<Integer, String> lineAndRemainingText = parseNextIntSkippingColon(text.substring(index)); if (lineAndRemainingText == null) return null; Pair<Integer, String> colonAndRemainingText = null; final String trimmedTail = lineAndRemainingText.second.trim(); if (trimmedTail.startsWith("pos ")) { colonAndRemainingText = parseNextIntSkippingColon(trimmedTail.substring("pos ".length())); } if (colonAndRemainingText == null) { return Couple.of(lineAndRemainingText.first, -1); } else { return Couple.of(lineAndRemainingText.first, colonAndRemainingText.first); } } @Nullable private static Pair<Integer, String> parseNextIntSkippingColon(final @NotNull String text) { // "12 whatever or ": 12 whatever" int index = 0; // skip leading colon if (text.length() > index && text.charAt(index) == ':') index++; // skip whitespaces while (text.length() > index && Character.isWhitespace(text.charAt(index))) index++; final int numberStartIndex = index; while (text.length() > index && text.charAt(index) >= '0' && text.charAt(index) <= '9') index++; final int numberEndIndex = index; if (numberStartIndex == numberEndIndex) return null; try { final int line = Integer.parseInt(text.substring(numberStartIndex, numberEndIndex)); final String remainingText = text.substring(numberEndIndex); return Pair.create(line, remainingText); } catch (NumberFormatException e) { return null; } } // trim all leading slashes on windows or all except one on Mac/Linux private static int getPathStartIndex(final @NotNull String text) { if (text.isEmpty() || text.charAt(0) != '/') return 0; int index = 0; while (index < text.length() && text.charAt(index) == '/') index++; return SystemInfo.isWindows ? index : index - 1; } }