blob: e86bfc59343c8ed0360b7a7482c3a55e3b0d2725 [file] [log] [blame]
// Copyright (c) 2014, 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.
library trydart.poi;
import 'dart:async' show
Completer,
Future;
import 'dart:io' as io;
import 'dart:convert' show
UTF8;
import 'package:dart2js_incremental/dart2js_incremental.dart' show
INCREMENTAL_OPTIONS,
reuseCompiler;
import 'package:dart2js_incremental/library_updater.dart' show
IncrementalCompilerContext,
LibraryUpdater;
import 'package:compiler/src/source_file_provider.dart' show
FormattingDiagnosticHandler;
import 'package:compiler/compiler.dart' as api;
import 'package:compiler/src/dart2jslib.dart' show
Compiler,
CompilerTask,
Enqueuer,
QueueFilter,
WorkItem;
import 'package:compiler/src/elements/visitor.dart' show
BaseElementVisitor;
import 'package:compiler/src/elements/elements.dart' show
AbstractFieldElement,
ClassElement,
CompilationUnitElement,
Element,
ElementCategory,
FunctionElement,
LibraryElement,
ScopeContainerElement;
import 'package:compiler/src/elements/modelx.dart' as modelx;
import 'package:compiler/src/elements/modelx.dart' show
DeclarationSite;
import 'package:compiler/src/dart_types.dart' show
DartType;
import 'package:compiler/src/scanner/scannerlib.dart' show
EOF_TOKEN,
IDENTIFIER_TOKEN,
KEYWORD_TOKEN,
PartialClassElement,
PartialElement,
Token;
import 'package:compiler/src/js/js.dart' show
js;
import 'scope_information_visitor.dart' show
ScopeInformationVisitor;
/// Enabled by the option --enable-dart-mind. Controls if this program should
/// be querying Dart Mind.
bool isDartMindEnabled = false;
/// Iterator over lines from standard input (or the argument array).
Iterator<String> stdin;
/// Enabled by the option --simulate-mutation. When true, this program will
/// only prompt for one file name, and subsequent runs will read
/// FILENAME.N.dart, where N starts at 1, and is increased on each iteration.
/// For example, if the program is invoked as:
///
/// dart poi.dart --simulate-mutation test.dart 11 22 33 44
///
/// The program will first read the file 'test.dart' and compute scope
/// information about position 11, then position 22 in test.dart.1.dart, then
/// position 33 in test.dart.2.dart, and finally position 44 in
/// test.dart.3.dart.
bool isSimulateMutationEnabled = false;
/// Counts the number of times [runPoi] has been invoked.
int poiCount;
int globalCounter = 0;
/// Enabled by the option --verbose (or -v). Prints more information than you
/// really need.
bool isVerbose = false;
/// Enabled by the option --compile. Also compiles the program after analyzing
/// the POI.
bool isCompiler = false;
/// Enabled by the option --minify. Passes the same option to the compiler to
/// generate minified output.
bool enableMinification = false;
/// When true (the default value) print serialized scope information at the
/// provided position.
const bool PRINT_SCOPE_INFO =
const bool.fromEnvironment('PRINT_SCOPE_INFO', defaultValue: true);
Stopwatch wallClock = new Stopwatch();
PoiTask poiTask;
Compiler cachedCompiler;
/// Iterator for reading lines from [io.stdin].
class StdinIterator implements Iterator<String> {
String current;
bool moveNext() {
current = io.stdin.readLineSync();
return true;
}
}
printFormattedTime(message, int us) {
String m = '$message${" " * 65}'.substring(0, 60);
String i = '${" " * 10}${(us/1000).toStringAsFixed(3)}';
i = i.substring(i.length - 10);
print('$m ${i}ms');
}
printWallClock(message) {
if (!isVerbose) return;
if (wallClock.isRunning) {
print('$message');
printFormattedTime('--->>>', wallClock.elapsedMicroseconds);
wallClock.reset();
} else {
print(message);
}
}
printVerbose(message) {
if (!isVerbose) return;
print(message);
}
main(List<String> arguments) {
poiCount = 0;
wallClock.start();
List<String> nonOptionArguments = [];
for (String argument in arguments) {
if (argument.startsWith('-')) {
switch (argument) {
case '--simulate-mutation':
isSimulateMutationEnabled = true;
break;
case '--enable-dart-mind':
isDartMindEnabled = true;
break;
case '-v':
case '--verbose':
isVerbose = true;
break;
case '--compile':
isCompiler = true;
break;
case '--minify':
enableMinification = true;
break;
default:
throw 'Unknown option: $argument.';
}
} else {
nonOptionArguments.add(argument);
}
}
if (nonOptionArguments.isEmpty) {
stdin = new StdinIterator();
} else {
stdin = nonOptionArguments.iterator;
}
FormattingDiagnosticHandler handler = new FormattingDiagnosticHandler();
handler
..verbose = false
..enableColors = true;
api.CompilerInputProvider inputProvider = handler.provider;
return prompt('Dart file: ').then((String fileName) {
if (isSimulateMutationEnabled) {
inputProvider = simulateMutation(fileName, inputProvider);
}
return prompt('Position: ').then((String position) {
return parseUserInput(fileName, position, inputProvider, handler);
});
});
}
/// Create an input provider that implements the behavior documented at
/// [simulateMutation].
api.CompilerInputProvider simulateMutation(
String fileName,
api.CompilerInputProvider inputProvider) {
Uri script = Uri.base.resolveUri(new Uri.file(fileName));
int count = poiCount;
Future cache;
String cachedFileName = script.toFilePath();
int counter = ++globalCounter;
return (Uri uri) {
if (counter != globalCounter) throw 'Using old provider';
printVerbose('fake inputProvider#$counter($uri): $poiCount $count');
if (uri == script) {
if (poiCount == count) {
cachedFileName = uri.toFilePath();
if (count != 0) {
cachedFileName = '$cachedFileName.$count.dart';
}
printVerbose('Not using cached version of $cachedFileName');
cache = new io.File(cachedFileName).readAsBytes().then((data) {
printVerbose(
'Read file $cachedFileName: '
'${UTF8.decode(data.take(100).toList(), allowMalformed: true)}...');
return data;
});
count++;
} else {
printVerbose('Using cached version of $cachedFileName');
}
return cache;
} else {
printVerbose('Using original provider for $uri');
return inputProvider(uri);
}
};
}
Future<String> prompt(message) {
if (stdin is StdinIterator) {
io.stdout.write(message);
}
return io.stdout.flush().then((_) {
stdin.moveNext();
return stdin.current;
});
}
Future queryDartMind(String prefix, String info) {
// TODO(lukechurch): Use [info] for something.
String encodedArg0 = Uri.encodeComponent('"$prefix"');
String mindQuery =
'http://dart-mind.appspot.com/rpc'
'?action=GetExportingPubCompletions'
'&arg0=$encodedArg0';
Uri uri = Uri.parse(mindQuery);
io.HttpClient client = new io.HttpClient();
return client.getUrl(uri).then((io.HttpClientRequest request) {
return request.close();
}).then((io.HttpClientResponse response) {
Completer<String> completer = new Completer<String>();
response.transform(UTF8.decoder).listen((contents) {
completer.complete(contents);
});
return completer.future;
});
}
Future parseUserInput(
String fileName,
String positionString,
api.CompilerInputProvider inputProvider,
api.DiagnosticHandler handler) {
Future repeat() {
printFormattedTime('--->>>', wallClock.elapsedMicroseconds);
wallClock.reset();
return prompt('Position: ').then((String positionString) {
wallClock.reset();
return parseUserInput(fileName, positionString, inputProvider, handler);
});
}
printWallClock("\n\n\nparseUserInput('$fileName', '$positionString')");
Uri script = Uri.base.resolveUri(new Uri.file(fileName));
if (positionString == null) return null;
int position = int.parse(
positionString, onError: (_) { print('Please enter an integer.'); });
if (position == null) return repeat();
inputProvider(script);
if (isVerbose) {
handler(
script, position, position + 1,
'Point of interest. '
'Cursor is immediately before highlighted character.',
api.Diagnostic.HINT);
}
Stopwatch sw = new Stopwatch()..start();
Future future = runPoi(script, position, inputProvider, handler);
return future.then((Element element) {
if (isVerbose) {
printFormattedTime('Resolving took', sw.elapsedMicroseconds);
}
sw.reset();
String info = scopeInformation(element, position);
sw.stop();
if (PRINT_SCOPE_INFO) {
print(info);
}
printVerbose('Scope information took ${sw.elapsedMicroseconds}us.');
sw..reset()..start();
Token token = findToken(element, position);
String prefix;
if (token != null) {
if (token.charOffset + token.charCount <= position) {
// After the token; in whitespace, or in the beginning of another token.
prefix = "";
} else if (token.kind == IDENTIFIER_TOKEN ||
token.kind == KEYWORD_TOKEN) {
prefix = token.value.substring(0, position - token.charOffset);
}
}
sw.stop();
printVerbose('Find token took ${sw.elapsedMicroseconds}us.');
if (isDartMindEnabled && prefix != null) {
sw..reset()..start();
return queryDartMind(prefix, info).then((String dartMindSuggestion) {
sw.stop();
print('Dart Mind ($prefix): $dartMindSuggestion.');
printVerbose('Dart Mind took ${sw.elapsedMicroseconds}us.');
return repeat();
});
} else {
if (isDartMindEnabled) {
print("Didn't talk to Dart Mind, no identifier at POI ($token).");
}
return repeat();
}
});
}
/// Find the token corresponding to [position] in [element]. The method only
/// works for instances of [PartialElement] or [LibraryElement]. Support for
/// [LibraryElement] is currently limited, and works only for named libraries.
Token findToken(modelx.ElementX element, int position) {
Token beginToken;
DeclarationSite site = element.declarationSite;
if (site is PartialElement) {
beginToken = site.beginToken;
} else if (element.isLibrary) {
// TODO(ahe): Generalize support for library elements (and update above
// documentation).
modelx.LibraryElementX lib = element;
var tag = lib.libraryTag;
if (tag != null) {
beginToken = tag.libraryKeyword;
}
} else {
beginToken = element.position;
}
if (beginToken == null) return null;
for (Token token = beginToken; token.kind != EOF_TOKEN; token = token.next) {
if (token.charOffset < position && position <= token.next.charOffset) {
return token;
}
}
return null;
}
Future<Element> runPoi(
Uri script,
int position,
api.CompilerInputProvider inputProvider,
api.DiagnosticHandler handler) {
Stopwatch sw = new Stopwatch()..start();
Uri libraryRoot = Uri.base.resolve('sdk/');
Uri packageRoot = Uri.base.resolveUri(
new Uri.file('${io.Platform.packageRoot}/'));
var options = [
'--analyze-main',
'--verbose',
'--categories=Client,Server',
];
options.addAll(INCREMENTAL_OPTIONS);
if (!isCompiler) {
options.add('--analyze-only');
}
if (enableMinification) {
options.add('--minify');
}
LibraryUpdater updater;
Future<bool> reuseLibrary(LibraryElement library) {
return poiTask.measure(() => updater.reuseLibrary(library));
}
Future<Compiler> invokeReuseCompiler() {
var context = new IncrementalCompilerContext();
updater = new LibraryUpdater(
cachedCompiler, inputProvider, printWallClock, printVerbose, context);
context.registerUriWithUpdates([script]);
return reuseCompiler(
diagnosticHandler: handler,
inputProvider: inputProvider,
options: options,
cachedCompiler: cachedCompiler,
libraryRoot: libraryRoot,
packageRoot: packageRoot,
packagesAreImmutable: true,
reuseLibrary: reuseLibrary);
}
return invokeReuseCompiler().then((Compiler newCompiler) {
// TODO(ahe): Move this "then" block to [reuseCompiler].
if (updater.failed) {
cachedCompiler = null;
return invokeReuseCompiler();
} else {
return newCompiler;
}
}).then((Compiler newCompiler) {
if (!isCompiler) {
newCompiler.enqueuerFilter = new ScriptOnlyFilter(script);
}
return runPoiInternal(newCompiler, sw, updater, script, position);
});
}
Future<Element> runPoiInternal(
Compiler newCompiler,
Stopwatch sw,
LibraryUpdater updater,
Uri uri,
int position) {
bool isFullCompile = cachedCompiler != newCompiler;
cachedCompiler = newCompiler;
if (poiTask == null || poiTask.compiler != cachedCompiler) {
poiTask = new PoiTask(cachedCompiler);
cachedCompiler.tasks.add(poiTask);
}
if (!isFullCompile) {
printFormattedTime(
'Analyzing changes and updating elements took', sw.elapsedMicroseconds);
}
sw.reset();
Future<bool> compilation;
if (updater.hasPendingUpdates) {
compilation = new Future(() {
var node = js.statement(
r'var $dart_patch = #', js.escapedString(updater.computeUpdateJs()));
print(updater.prettyPrintJs(node));
return !cachedCompiler.compilationFailed;
});
} else {
compilation = cachedCompiler.run(uri);
}
return compilation.then((success) {
printVerbose('Compiler queue processed in ${sw.elapsedMicroseconds}us');
if (isVerbose) {
for (final task in cachedCompiler.tasks) {
int time = task.timingMicroseconds;
if (time != 0) {
printFormattedTime('${task.name} took', time);
}
}
}
if (poiCount != null) poiCount++;
if (success != true) {
throw 'Compilation failed';
}
return findPosition(position, cachedCompiler.mainApp);
});
}
Element findPosition(int position, Element element) {
FindPositionVisitor visitor = new FindPositionVisitor(position, element);
element.accept(visitor, null);
return visitor.element;
}
String scopeInformation(Element element, int position) {
ScopeInformationVisitor visitor =
new ScopeInformationVisitor(cachedCompiler, element, position);
element.accept(visitor, null);
return '${visitor.buffer}';
}
class FindPositionVisitor extends BaseElementVisitor {
final int position;
Element element;
FindPositionVisitor(this.position, this.element);
visitElement(modelx.ElementX e, _) {
DeclarationSite site = e.declarationSite;
if (site is PartialElement) {
if (site.beginToken.charOffset <= position &&
position < site.endToken.next.charOffset) {
element = e;
}
}
}
visitClassElement(ClassElement e, _) {
if (e is PartialClassElement) {
if (e.beginToken.charOffset <= position &&
position < e.endToken.next.charOffset) {
element = e;
visitScopeContainerElement(e, _);
}
}
}
visitScopeContainerElement(ScopeContainerElement e, _) {
e.forEachLocalMember((Element element) => element.accept(this, _));
}
}
class ScriptOnlyFilter implements QueueFilter {
final Uri script;
ScriptOnlyFilter(this.script);
bool checkNoEnqueuedInvokedInstanceMethods(Enqueuer enqueuer) => true;
void processWorkItem(void f(WorkItem work), WorkItem work) {
if (work.element.library.canonicalUri != script) {
// TODO(ahe): Rather nasty hack to work around another nasty hack in
// backend.dart. Find better solution.
if (work.element.name != 'closureFromTearOff') {
printWallClock('Skipped ${work.element}.');
return;
}
}
f(work);
printWallClock('Processed ${work.element}.');
}
}
class PoiTask extends CompilerTask {
PoiTask(Compiler compiler) : super(compiler);
String get name => 'POI';
}