| #!/usr/bin/env dart | |
| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:kernel/kernel.dart'; | |
| import 'package:kernel/naive_type_checker.dart'; | |
| import 'package:kernel/text/ast_to_text.dart'; | |
| class ErrorFormatter implements FailureListener { | |
| List<String> failures = <String>[]; | |
| int get numberOfFailures => failures.length; | |
| @override | |
| void reportNotAssignable(TreeNode where, DartType from, DartType to) { | |
| reportFailure( | |
| where, | |
| '${ansiBlue}${from}${ansiReset} ${ansiYellow}is not assignable to' | |
| '${ansiReset} ${ansiBlue}${to}${ansiReset}'); | |
| } | |
| @override | |
| void reportInvalidOverride( | |
| Member ownMember, Member superMember, String message) { | |
| reportFailure(ownMember, ''' | |
| Incompatible override of ${superMember} with ${ownMember}: | |
| ${_realign(message, ' ')}'''); | |
| } | |
| @override | |
| void reportFailure(TreeNode where, String message) { | |
| final dynamic context = where is Class || where is Library | |
| ? where | |
| : _findEnclosingMember(where); | |
| String sourceLocation = '<unknown source>'; | |
| String sourceLine = null; | |
| // Try finding original source line. | |
| final fileOffset = _findFileOffset(where); | |
| if (fileOffset != TreeNode.noOffset) { | |
| final fileUri = _fileUriOf(context); | |
| final component = context.enclosingComponent; | |
| final source = component.uriToSource[fileUri]; | |
| final location = component.getLocation(fileUri, fileOffset); | |
| final lineStart = source.lineStarts[location.line - 1]; | |
| final lineEnd = (location.line < source.lineStarts.length) | |
| ? source.lineStarts[location.line] | |
| : (source.source.length - 1); | |
| if (lineStart < source.source.length && | |
| lineEnd < source.source.length && | |
| lineStart < lineEnd) { | |
| sourceLocation = '${fileUri}:${location.line}'; | |
| sourceLine = new String.fromCharCodes( | |
| source.source.getRange(lineStart, lineEnd)); | |
| } | |
| } | |
| // Find the name of the enclosing member. | |
| var name = "", body = context; | |
| if (context is Class || context is Library) { | |
| name = context.name; | |
| } else if (context is Procedure || context is Constructor) { | |
| final parent = context.parent; | |
| final parentName = | |
| parent is Class ? parent.name : (parent as Library).name; | |
| name = "${parentName}::${context.name.name}"; | |
| } else { | |
| final field = context as Field; | |
| if (where is Field) { | |
| name = "${field.parent}.${field.name}"; | |
| } else { | |
| name = "field initializer for ${field.parent}.${field.name}"; | |
| } | |
| } | |
| String failure = ''' | |
| ----------------------------------------------------------------------- | |
| In ${name} at ${sourceLocation}: | |
| ${message.replaceAll('\n', '\n ')} | |
| Kernel: | |
| | | |
| | ${_realign(HighlightingPrinter.stringifyContainingLines(body, where))} | |
| | | |
| '''; | |
| if (sourceLine != null) { | |
| failure = '''$failure | |
| Source: | |
| | | |
| | ${_realign(sourceLine)} | |
| | | |
| '''; | |
| } | |
| failures.add(failure); | |
| } | |
| static Uri _fileUriOf(FileUriNode node) { | |
| return node.fileUri; | |
| } | |
| static String _realign(String str, [String prefix = '| ']) => | |
| str.trimRight().replaceAll('\n', '\n${prefix}'); | |
| static int _findFileOffset(TreeNode context) { | |
| while (context != null && context.fileOffset == TreeNode.noOffset) { | |
| context = context.parent; | |
| } | |
| return context?.fileOffset ?? TreeNode.noOffset; | |
| } | |
| static Member _findEnclosingMember(TreeNode n) { | |
| var context = n; | |
| while (context is! Member) { | |
| context = context.parent; | |
| } | |
| return context; | |
| } | |
| } | |
| /// Extension of a [Printer] that highlights the given node using ANSI | |
| /// escape sequences. | |
| class HighlightingPrinter extends Printer { | |
| final highlight; | |
| HighlightingPrinter(this.highlight) | |
| : super(new StringBuffer(), syntheticNames: globalDebuggingNames); | |
| @override | |
| bool shouldHighlight(Node node) => highlight == node; | |
| static const kHighlightStart = ansiRed; | |
| static const kHighlightEnd = ansiReset; | |
| @override | |
| void startHighlight(Node node) { | |
| sink.write(kHighlightStart); | |
| } | |
| @override | |
| void endHighlight(Node node) { | |
| sink.write(kHighlightEnd); | |
| } | |
| /// Stringify the given [node] but only return lines that contain string | |
| /// representation of the [highlight] node. | |
| static String stringifyContainingLines(Node node, Node highlight) { | |
| if (node == highlight) { | |
| final firstLine = debugNodeToString(node).split('\n').first; | |
| return "${kHighlightStart}${firstLine}${kHighlightEnd}"; | |
| } | |
| final HighlightingPrinter p = new HighlightingPrinter(highlight); | |
| p.writeNode(node); | |
| final String text = p.sink.toString(); | |
| return _onlyHighlightedLines(text).join('\n'); | |
| } | |
| static Iterable<String> _onlyHighlightedLines(String text) sync* { | |
| for (var line | |
| in text.split('\n').skipWhile((l) => !l.contains(kHighlightStart))) { | |
| yield line; | |
| if (line.contains(kHighlightEnd)) { | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| const ansiBlue = "\u001b[1;34m"; | |
| const ansiYellow = "\u001b[1;33m"; | |
| const ansiRed = "\u001b[1;31m"; | |
| const ansiReset = "\u001b[0;0m"; |