| // Copyright (c) 2020, 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 'dart:async'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'dart:math' as math; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:args/args.dart'; | |
| import 'package:language_server_protocol/protocol_generated.dart' as lsp; | |
| import 'package:path/path.dart' as p; | |
| import 'core.dart'; | |
| import 'sdk.dart'; | |
| /// For commands where we are able to initialize the [ArgParser], this value | |
| /// is used as the usageLineLength. | |
| int? get dartdevUsageLineLength => | |
| stdout.hasTerminal ? stdout.terminalColumns : null; | |
| /// Runs a tool's snapshot in an isolate. | |
| /// | |
| /// Waits for the spawned isolate to exit before returning. | |
| Future<int> runFromSnapshot({ | |
| required String snapshot, | |
| required List<String> args, | |
| required bool verbose, | |
| }) async { | |
| if (!checkArtifactExists(snapshot)) return 255; | |
| int retval = 0; | |
| final result = Completer<int>(); | |
| final exitPort = ReceivePort() | |
| ..listen((msg) { | |
| result.complete(0); | |
| }); | |
| final errorPort = ReceivePort() | |
| ..listen((error) { | |
| log.stderr(error.toString()); | |
| result.complete(255); | |
| }); | |
| try { | |
| await Isolate.spawnUri( | |
| Uri.file(snapshot), | |
| args, | |
| null, | |
| onExit: exitPort.sendPort, | |
| onError: errorPort.sendPort, | |
| ); | |
| retval = await result.future; | |
| } catch (e, st) { | |
| log.stderr(e.toString()); | |
| if (verbose) { | |
| log.stderr(st.toString()); | |
| } | |
| retval = 255; | |
| } | |
| errorPort.close(); | |
| exitPort.close(); | |
| return retval; | |
| } | |
| /// Global options for dartdev. | |
| /// | |
| /// ** READ THIS BEFORE MODIFYING ** | |
| /// | |
| /// Adding or changing behavior for global flags may have consequences for | |
| /// integration with the VM. Check `runtime/bin/main_options.cc` in the | |
| /// Dart SDK if adding or changing any flags. This is most important for | |
| /// those that are intended to be run without a script such as | |
| /// `dart --disable-analytics` as there is special handling. Any flags | |
| /// added here should also be tested by hand with a compiled SDK as unit tests | |
| /// running `dartdev.dart` directly do not hit that code path. | |
| /// | |
| /// The option name for inline code evaluation. | |
| const String evalOption = 'eval'; | |
| /// The option name for specifying package dependencies for inline evaluation. | |
| const String packageConstraintOption = 'package-constraint'; | |
| /// The option name for offline pub resolution. | |
| const String offlineOption = 'offline'; | |
| /// The MIME type for in-memory Dart evaluation data URIs. | |
| const String dartMimeType = 'application/dart'; | |
| ArgParser globalDartdevOptionsParser({bool verbose = false}) { | |
| var argParser = ArgParser( | |
| usageLineLength: dartdevUsageLineLength, | |
| allowTrailingOptions: false, | |
| ); | |
| argParser.addFlag( | |
| 'verbose', | |
| abbr: 'v', | |
| negatable: false, | |
| help: 'Show additional command output.', | |
| ); | |
| argParser.addFlag( | |
| 'version', | |
| negatable: false, | |
| help: 'Print the Dart SDK version.', | |
| ); | |
| argParser.addFlag( | |
| 'enable-analytics', | |
| negatable: false, | |
| help: 'Enable analytics.', | |
| ); | |
| argParser.addFlag( | |
| 'disable-analytics', | |
| negatable: false, | |
| help: 'Disable analytics.', | |
| ); | |
| argParser.addFlag( | |
| 'disable-telemetry', | |
| negatable: false, | |
| help: 'Disable telemetry.', | |
| hide: true, | |
| ); | |
| argParser.addFlag( | |
| 'diagnostics', | |
| negatable: false, | |
| help: 'Show tool diagnostic output.', | |
| hide: !verbose, | |
| ); | |
| argParser.addFlag( | |
| 'analytics', | |
| defaultsTo: true, | |
| negatable: true, | |
| help: | |
| 'Allow or disallow analytics for this `dart *` run without ' | |
| 'changing the analytics configuration. ' | |
| 'Deprecated: use `--suppress-analytics` instead.', | |
| hide: true, | |
| ); | |
| argParser.addFlag( | |
| 'suppress-analytics', | |
| negatable: false, | |
| help: | |
| 'Disallow analytics for this `dart *` run without changing the ' | |
| 'analytics configuration.', | |
| ); | |
| return argParser; | |
| } | |
| /// Emit the given word with the correct pluralization. | |
| String pluralize(String word, int count) => count == 1 ? word : '${word}s'; | |
| /// Make an absolute [filePath] relative to [dir] (for display purposes). | |
| String relativePath(String filePath, Directory dir) { | |
| var root = dir.absolute.path; | |
| if (filePath.startsWith(root)) { | |
| return filePath.substring(root.length + 1); | |
| } | |
| return filePath; | |
| } | |
| /// String utility to trim some suffix from the end of a [String]. | |
| String trimEnd(String s, String? suffix) { | |
| if (suffix != null && suffix.isNotEmpty && s.endsWith(suffix)) { | |
| return s.substring(0, s.length - suffix.length); | |
| } | |
| return s; | |
| } | |
| extension FileSystemEntityExtension on FileSystemEntity { | |
| String get name => p.basename(path); | |
| String get basenameWithoutExtension => p.basenameWithoutExtension(path); | |
| bool get isDartFile => this is File && p.extension(path) == '.dart'; | |
| } | |
| extension LineInfoPositionExtension on LineInfo { | |
| /// Converts a 0-indexed LSP [position] into a flat character offset using | |
| /// this [LineInfo]. | |
| int offsetOfPosition(lsp.Position position) { | |
| if (position.line < 0) return 0; | |
| if (position.line >= lineCount) { | |
| return lineStarts.isNotEmpty ? lineStarts.last : 0; | |
| } | |
| return getOffsetOfLine(position.line) + position.character; | |
| } | |
| } | |
| /// Extension methods for working with package URIs. | |
| extension PackageUriExtension on Uri { | |
| /// The package name if this is a valid `package:` URI with a non-empty | |
| /// package name, or `null`. | |
| String? get packageName { | |
| if (!isScheme('package')) return null; | |
| final first = pathSegments.firstOrNull; | |
| return (first != null && first.isNotEmpty) ? first : null; | |
| } | |
| /// Whether this is a valid `package:` URI with a non-empty package name. | |
| bool get isPackage => packageName != null; | |
| } | |
| /// Wraps [text] to the given [width], if provided. | |
| String wrapText(String text, {int? width}) { | |
| if (width == null) { | |
| return text; | |
| } | |
| final isAnsi = List<bool>.filled(text.length, false); | |
| final matches = _ansiEscapeRegExp.allMatches(text); | |
| for (final match in matches) { | |
| for (var i = match.start; i < match.end; i++) { | |
| isAnsi[i] = true; | |
| } | |
| } | |
| final visualBuffer = StringBuffer(); | |
| for (var i = 0; i < text.length; i++) { | |
| if (!isAnsi[i]) { | |
| visualBuffer.write(text[i]); | |
| } | |
| } | |
| final visualString = visualBuffer.toString(); | |
| final visualSplits = <int>{}; | |
| var lineMaxEndIndex = width; | |
| var lineStartIndex = 0; | |
| while (true) { | |
| if (lineMaxEndIndex >= visualString.length) { | |
| break; | |
| } else { | |
| var lastSpaceIndex = visualString.lastIndexOf(' ', lineMaxEndIndex); | |
| if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) { | |
| // No space between [lineStartIndex] and [lineMaxEndIndex]. Get the | |
| // _next_ space. | |
| lastSpaceIndex = visualString.indexOf(' ', lineMaxEndIndex); | |
| if (lastSpaceIndex == -1) { | |
| break; | |
| } | |
| } | |
| visualSplits.add(lastSpaceIndex); | |
| lineStartIndex = lastSpaceIndex + 1; | |
| } | |
| lineMaxEndIndex = lineStartIndex + width; | |
| } | |
| final buffer = StringBuffer(); | |
| var v = 0; | |
| for (var i = 0; i < text.length; i++) { | |
| if (isAnsi[i]) { | |
| buffer.write(text[i]); | |
| } else { | |
| if (visualSplits.contains(v)) { | |
| buffer.writeln(); | |
| } else { | |
| buffer.write(text[i]); | |
| } | |
| v++; | |
| } | |
| } | |
| return buffer.toString(); | |
| } | |
| // A valid Dart identifier that can be used for a package, i.e. no | |
| // capital letters. | |
| // https://dart.dev/language#important-concepts | |
| final RegExp _identifierRegExp = RegExp(r'^[a-z_][a-z\d_]*$'); | |
| final RegExp _ansiEscapeRegExp = RegExp(r'\x1B\[[0-9;]*[a-zA-Z]'); | |
| // non-contextual dart keywords. | |
| // https://dart.dev/language/keywords | |
| const Set<String> _keywords = <String>{ | |
| 'abstract', | |
| 'as', | |
| 'assert', | |
| 'async', | |
| 'await', | |
| 'break', | |
| 'case', | |
| 'catch', | |
| 'class', | |
| 'const', | |
| 'continue', | |
| 'covariant', | |
| 'default', | |
| 'deferred', | |
| 'do', | |
| 'dynamic', | |
| 'else', | |
| 'enum', | |
| 'export', | |
| 'extends', | |
| 'extension', | |
| 'external', | |
| 'factory', | |
| 'false', | |
| 'final', | |
| 'finally', | |
| 'for', | |
| 'function', | |
| 'get', | |
| 'hide', | |
| 'if', | |
| 'implements', | |
| 'import', | |
| 'in', | |
| 'inout', | |
| 'interface', | |
| 'is', | |
| 'late', | |
| 'library', | |
| 'mixin', | |
| 'native', | |
| 'new', | |
| 'null', | |
| 'of', | |
| 'on', | |
| 'operator', | |
| 'out', | |
| 'part', | |
| 'patch', | |
| 'required', | |
| 'rethrow', | |
| 'return', | |
| 'set', | |
| 'show', | |
| 'source', | |
| 'static', | |
| 'super', | |
| 'switch', | |
| 'sync', | |
| 'this', | |
| 'throw', | |
| 'true', | |
| 'try', | |
| 'typedef', | |
| 'var', | |
| 'void', | |
| 'while', | |
| 'with', | |
| 'yield', | |
| }; | |
| /// Whether [name] is a valid Pub package. | |
| bool isValidPackageName(String name) => | |
| _identifierRegExp.hasMatch(name) && !_keywords.contains(name); | |
| /// RegExp used for converting camel case to snake case in | |
| /// [projectNameToLowerCase]. | |
| final _toSnakeCaseRegExp = RegExp('_?[A-Z]'); | |
| /// Converts [name] to lower case only. | |
| String projectNameToLowerCase(String name) { | |
| if (name.toLowerCase() == name) { | |
| // No conversion needed. | |
| return name; | |
| } else if (name.toUpperCase() == name) { | |
| // Convert all uppercase to lowercase. | |
| return name.toLowerCase(); | |
| } else { | |
| // This is copied from `pkg/analyzer_utilities/lib/extensions/string.dart`: | |
| // Convert mixed case to snake case, as if input is camel case. | |
| var parts = <String>[]; | |
| var i = 0; | |
| var wordStarts = _toSnakeCaseRegExp.allMatches(name); | |
| for (var RegExpMatch(:start) in wordStarts) { | |
| if (i < start) { | |
| parts.add(name.substring(i, start).toLowerCase()); | |
| i = start; | |
| } | |
| if (name[i] == '_' && parts.isNotEmpty) { | |
| // Avoid doubling up the `_`. This handles strings that are already in | |
| // snake case like `foo_Bar` (which translates to `foo_bar`). | |
| i++; | |
| } | |
| } | |
| if (i < name.length) { | |
| parts.add(name.substring(i).toLowerCase()); | |
| } | |
| return parts.join('_'); | |
| } | |
| } | |
| /// Convert a directory name into a reasonably legal pub package name. | |
| String normalizeProjectName(String name) { | |
| name = projectNameToLowerCase(name).replaceAll('-', '_').replaceAll(' ', '_'); | |
| // Strip any extension (like .dart). | |
| var dotIndex = name.indexOf('.'); | |
| if (dotIndex >= 0) { | |
| name = name.substring(0, dotIndex); | |
| } | |
| return name; | |
| } | |
| /// A utility class to generate a markdown table into a string. | |
| /// | |
| /// To use this class: | |
| /// | |
| /// ``` | |
| /// var table = MarkdownTable(); | |
| /// for (var foo in foos) { | |
| /// table.startRow() | |
| /// ..cell(foo.bar) | |
| /// ..cell(foo.baz.toStringAsFixed(1), right: true) | |
| /// ..cell(foo.qux); | |
| /// } | |
| /// print(table.finish()); | |
| /// ``` | |
| class MarkdownTable { | |
| static const int defaultMaxWidth = 90; | |
| static const int _minWidth = 3; | |
| final List<List<_MarkdownCell>> _data = []; | |
| MarkdownRow startRow() { | |
| _data.add([]); | |
| return MarkdownRow(this); | |
| } | |
| String finish() { | |
| if (_data.isEmpty) return ''; | |
| var header = _data.first; | |
| var widths = <int>[]; | |
| for (int col = 0; col < header.length; col++) { | |
| var width = _data | |
| .map((row) { | |
| var item = row.length >= col ? row[col] : null; | |
| return item?.value.length ?? 0; | |
| }) | |
| .reduce(math.max); | |
| widths.add(math.max(width, _minWidth)); | |
| } | |
| var buffer = StringBuffer(); | |
| for (var row in _data) { | |
| buffer.write('| '); | |
| for (int col = 0; col < row.length; col++) { | |
| if (col != 0) buffer.write(' | '); | |
| var cell = row[col]; | |
| var width = math.min(widths[col], defaultMaxWidth); | |
| var value = cell.value; | |
| buffer.write(cell.right ? value.padLeft(width) : value.padRight(width)); | |
| } | |
| buffer.writeln(' |'); | |
| if (row == _data.first) { | |
| // Write the alignment row. | |
| buffer.write('| '); | |
| for (int col = 0; col < row.length; col++) { | |
| if (col != 0) buffer.write(' | '); | |
| var cell = row[col]; | |
| var width = math.min(widths[col], defaultMaxWidth); | |
| var value = cell.right ? '--:' : '---'; | |
| buffer.write(value.padLeft(width, '-')); | |
| } | |
| buffer.writeln(' |'); | |
| } | |
| } | |
| return buffer.toString(); | |
| } | |
| } | |
| /// Used to build a row in a markdown table. | |
| class MarkdownRow { | |
| final MarkdownTable _table; | |
| MarkdownRow(this._table); | |
| void cell(String value, {bool right = false}) { | |
| _table._data.last.add(_MarkdownCell(value, right)); | |
| } | |
| } | |
| class _MarkdownCell { | |
| final String value; | |
| final bool right; | |
| _MarkdownCell(this.value, this.right); | |
| } | |
| /// The lowest macOS version that Dart supports. | |
| /// | |
| /// From https://dart.dev/get-dart#system-requirements | |
| const minimumSupportedMacOSVersion = 12; |