blob: 593bc59d274c6dc67881149d7c5933c6099234bd [file] [log] [blame]
// Copyright (c) 2015, 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 services.common_server;
import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:logging/logging.dart';
import 'package:rpc/rpc.dart';
import '../version.dart';
import 'analysis_server.dart';
import 'analyzer.dart';
import 'api_classes.dart';
import 'common.dart';
import 'compiler.dart';
import 'pub.dart';
import 'summarize.dart';
final Duration _standardExpiration = new Duration(hours: 1);
final Logger _logger = new Logger('common_server');
/// Toggle to on to enable `package:` support.
final bool enablePackages = false;
abstract class ServerCache {
Future<String> get(String key);
Future set(String key, String value, {Duration expiration});
Future remove(String key);
}
abstract class ServerContainer {
String get version;
}
/**
* Define a seperate class for source recording to provide a clearly
* defined schema
*/
abstract class SourceRequestRecorder {
Future record(String verb, String source, [int offset]);
}
class SummaryText {
String text;
SummaryText.fromString(String this.text);
}
abstract class PersistentCounter {
Future increment(String name, {int increment: 1});
Future<int> getTotal(String name);
}
@ApiClass(name: 'dartservices', version: 'v1')
class CommonServer {
final ServerContainer container;
final ServerCache cache;
final SourceRequestRecorder srcRequestRecorder;
final PersistentCounter counter;
Pub pub;
Analyzer strongModeAnalyzer;
Analyzer analyzer;
Compiler compiler;
AnalysisServerWrapper analysisServer;
CommonServer(String sdkPath, this.container, this.cache,
this.srcRequestRecorder, this.counter) {
hierarchicalLoggingEnabled = true;
_logger.level = Level.ALL;
pub = enablePackages ? new Pub() : new Pub.mock();
strongModeAnalyzer = new Analyzer(sdkPath, strongMode: true);
analyzer = new Analyzer(sdkPath);
compiler = new Compiler(sdkPath, pub);
analysisServer = new AnalysisServerWrapper(sdkPath);
}
Future warmup([bool useHtml = false]) async {
await analyzer.warmup(useHtml);
await strongModeAnalyzer.warmup(useHtml);
await compiler.warmup(useHtml);
await analysisServer.warmup(useHtml);
}
Future shutdown() => analysisServer.shutdown();
@ApiMethod(method: 'GET', path: 'counter')
Future<CounterResponse> counterGet({String name}) {
return counter.getTotal(name).then((total) {
return new CounterResponse(total);
});
}
@ApiMethod(
method: 'POST',
path: 'analyze',
description:
'Analyze the given Dart source code and return any resulting '
'analysis errors or warnings.')
Future<AnalysisResults> analyze(SourceRequest request) {
return _analyze(request.source, request.strongMode);
}
@ApiMethod(
method: 'POST',
path: 'analyzeMulti',
description:
'Analyze the given Dart source code and return any resulting '
'analysis errors or warnings.')
Future<AnalysisResults> analyzeMulti(SourcesRequest request) {
return _analyzeMulti(request.sources, request.strongMode);
}
@ApiMethod(
method: 'POST',
path: 'summarize',
description:
'Summarize the given Dart source code and return any resulting '
'analysis errors or warnings.')
Future<SummaryText> summarize(SourcesRequest request) {
return _summarize(request.sources['dart'], request.sources['css'],
request.sources['html']);
}
@ApiMethod(method: 'GET', path: 'analyze')
Future<AnalysisResults> analyzeGet({String source, bool strongMode: false}) {
return _analyze(source, strongMode);
}
@ApiMethod(
method: 'POST',
path: 'compile',
description: 'Compile the given Dart source code and return the '
'resulting JavaScript.')
Future<CompileResponse> compile(CompileRequest request) =>
_compile(request.source,
useCheckedMode: request.useCheckedMode,
returnSourceMap: request.returnSourceMap);
@ApiMethod(method: 'GET', path: 'compile')
Future<CompileResponse> compileGet({String source}) => _compile(source);
@ApiMethod(
method: 'POST',
path: 'complete',
description:
'Get the valid code completion results for the given offset.')
Future<CompleteResponse> complete(SourceRequest request) {
if (request.offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _complete(request.source, request.offset);
}
@ApiMethod(
method: 'POST',
path: 'completeMulti',
description:
'Get the valid code completion results for the given offset.')
Future<CompleteResponse> completeMulti(SourcesRequest request) {
if (request.location == null) {
throw new BadRequestError('Missing parameter: \'location\'');
}
return _completeMulti(
request.sources, request.location.sourceName, request.location.offset);
}
@ApiMethod(method: 'GET', path: 'complete')
Future<CompleteResponse> completeGet({String source, int offset}) {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _complete(source, offset);
}
@ApiMethod(
method: 'POST',
path: 'fixes',
description: 'Get any quick fixes for the given source code location.')
Future<FixesResponse> fixes(SourceRequest request) {
if (request.offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _fixes(request.source, request.offset);
}
@ApiMethod(
method: 'POST',
path: 'fixesMulti',
description: 'Get any quick fixes for the given source code location.')
Future<FixesResponse> fixesMulti(SourcesRequest request) {
if (request.location.sourceName == null) {
throw new BadRequestError('Missing parameter: \'fullName\'');
}
if (request.location.offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _fixesMulti(
request.sources, request.location.sourceName, request.location.offset);
}
@ApiMethod(method: 'GET', path: 'fixes')
Future<FixesResponse> fixesGet({String source, int offset}) {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _fixes(source, offset);
}
@ApiMethod(
method: 'POST',
path: 'format',
description: 'Format the given Dart source code and return the results. '
'If an offset is supplied in the request, the new position for that '
'offset in the formatted code will be returned.')
Future<FormatResponse> format(SourceRequest request) {
return _format(request.source, offset: request.offset);
}
@ApiMethod(method: 'GET', path: 'format')
Future<FormatResponse> formatGet({String source, int offset}) {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
return _format(source, offset: offset);
}
@ApiMethod(
method: 'POST',
path: 'document',
description: 'Return the relevant dartdoc information for the element at '
'the given offset.')
Future<DocumentResponse> document(SourceRequest request) {
return _document(request.source, request.offset);
}
@ApiMethod(method: 'GET', path: 'document')
Future<DocumentResponse> documentGet({String source, int offset}) {
return _document(source, offset);
}
@ApiMethod(
method: 'GET',
path: 'version',
description: 'Return the current SDK version for DartServices.')
Future<VersionResponse> version() => new Future.value(_version());
Future<AnalysisResults> _analyze(String source, bool strongMode) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
return _analyzeMulti({"main.dart": source}, strongMode);
}
Future<SummaryText> _summarize(String dart, String html, String css) async {
if (dart == null || html == null || css == null) {
throw new BadRequestError('Missing core source parameter.');
}
String sourcesJson =
new JsonEncoder().convert({"dart": dart, "html": html, "css": css});
_logger.info("About to summarize: ${_hashSource(sourcesJson)}");
SummaryText summaryString =
await _analyzeMulti({"main.dart": dart}, false).then((result) {
Summarizer summarizer =
new Summarizer(dart: dart, html: html, css: css, analysis: result);
return new SummaryText.fromString(summarizer.returnAsSimpleSummary());
});
return new Future.value(summaryString);
}
Future<AnalysisResults> _analyzeMulti(
Map<String, String> sources, bool strongMode) async {
if (sources == null) {
throw new BadRequestError('Missing parameter: \'sources\'');
}
strongMode ??= false;
Stopwatch watch = new Stopwatch()..start();
String sourcesJson = new JsonEncoder().convert(sources);
srcRequestRecorder.record("ANALYZE-v2-$strongMode", sourcesJson);
_logger.info("About to ANALYZE-v1: ${_hashSource(sourcesJson)}");
// Select the right analyzer
Analyzer selectedAnalyzer = strongMode ? strongModeAnalyzer : analyzer;
try {
return selectedAnalyzer
.analyzeMulti(sources)
.then((AnalysisResults results) async {
int lineCount = 0;
sources.values
.forEach((String source) => lineCount += source.split('\n').length);
int ms = watch.elapsedMilliseconds;
_logger.info('PERF: Analyzed ${lineCount} lines of Dart in ${ms}ms.');
counter.increment("Analyses");
counter.increment("Analyzed-Lines", increment: lineCount);
return results;
}).catchError((e) {
_logger.severe('Error during analyze: ${e}');
throw e;
});
} catch (e, st) {
_logger.severe('Error during analyze: ${e}\n${st}');
throw e;
}
}
Future<CompileResponse> _compile(String source,
{bool useCheckedMode, bool returnSourceMap}) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (useCheckedMode == null) useCheckedMode = false;
if (returnSourceMap == null) returnSourceMap = false;
srcRequestRecorder.record("COMPILE", source);
String sourceHash = _hashSource(source);
_logger.info("About to COMPILE: ${sourceHash}");
// TODO(lukechurch): Remove this hack after
// https://github.com/dart-lang/rpc/issues/15 lands
var trimSrc = source.trim();
bool suppressCache = trimSrc.endsWith("/** Supress-Memcache **/") ||
trimSrc.endsWith("/** Suppress-Memcache **/");
String memCacheKey = "%%COMPILE:v0:useCheckedMode:$useCheckedMode"
"returnSourceMap:$returnSourceMap:"
"source:$sourceHash";
return checkCache(memCacheKey).then((String result) {
if (!suppressCache && result != null) {
_logger.info("CACHE: Cache hit for compile");
var resultObj = new JsonDecoder().convert(result);
return new CompileResponse(resultObj["output"],
returnSourceMap ? resultObj["sourceMap"] : null);
} else {
_logger.info("CACHE: MISS, forced: $suppressCache");
Stopwatch watch = new Stopwatch()..start();
return compiler
.compile(source,
useCheckedMode: useCheckedMode,
returnSourceMap: returnSourceMap)
.then((CompilationResults results) async {
if (results.hasOutput) {
int lineCount = source.split('\n').length;
int outputSize = (results.getOutput().length + 512) ~/ 1024;
int ms = watch.elapsedMilliseconds;
_logger.info('PERF: Compiled ${lineCount} lines of Dart into '
'${outputSize}kb of JavaScript in ${ms}ms.');
counter.increment("Compilations");
counter.increment("Compiled-Lines", increment: lineCount);
String out = results.getOutput();
String sourceMap = returnSourceMap ? results.getSourceMap() : null;
String cachedResult = new JsonEncoder()
.convert({"output": out, "sourceMap": sourceMap});
await setCache(memCacheKey, cachedResult);
return new CompileResponse(out, sourceMap);
} else {
List problems = results.problems;
String errors = problems.map(_printCompileProblem).join('\n');
throw new BadRequestError(errors);
}
}).catchError((e, st) {
_logger.severe('Error during compile: ${e}\n${st}');
throw e;
});
}
});
}
Future<DocumentResponse> _document(String source, int offset) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
Stopwatch watch = new Stopwatch()..start();
srcRequestRecorder.record("DOCUMENT", source, offset);
_logger.info("About to DOCUMENT: ${_hashSource(source)}");
try {
return analyzer
.dartdoc(source, offset)
.then((Map<String, String> docInfo) async {
if (docInfo == null) docInfo = {};
_logger
.info('PERF: Computed dartdoc in ${watch.elapsedMilliseconds}ms.');
counter.increment("DartDocs");
return new DocumentResponse(docInfo);
}).catchError((e, st) {
_logger.severe('Error during dartdoc: ${e}\n${st}');
throw e;
});
} catch (e, st) {
_logger.severe('Error during dartdoc: ${e}\n${st}');
throw e;
}
}
VersionResponse _version() => new VersionResponse(
sdkVersion: compiler.version,
sdkVersionFull: compiler.versionFull,
runtimeVersion: vmVersion,
servicesVersion: servicesVersion,
appEngineVersion: container.version);
Future<CompleteResponse> _complete(String source, int offset) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _completeMulti({"main.dart": source}, "main.dart", offset);
}
Future<CompleteResponse> _completeMulti(
Map<String, String> sources, String sourceName, int offset) async {
if (sources == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (sourceName == null) {
throw new BadRequestError('Missing parameter: \'name\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
Stopwatch watch = new Stopwatch()..start();
String sourceJson = new JsonEncoder().convert(sources);
srcRequestRecorder.record("COMPLETE-v1", sourceJson, offset);
_logger.info("About to COMPLETE-v1: ${_hashSource(sourceJson)}");
counter.increment("Completions");
var response = await analysisServer.completeMulti(
sources,
new Location()
..sourceName = sourceName
..offset = offset);
_logger
.info('PERF: Computed completions in ${watch.elapsedMilliseconds}ms.');
return response;
}
Future<FixesResponse> _fixes(String source, int offset) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
return _fixesMulti({"main.dart": source}, "main.dart", offset);
}
Future<FixesResponse> _fixesMulti(
Map<String, String> sources, String sourceName, int offset) async {
if (sources == null) {
throw new BadRequestError('Missing parameter: \'sources\'');
}
if (offset == null) {
throw new BadRequestError('Missing parameter: \'offset\'');
}
Stopwatch watch = new Stopwatch()..start();
String sourceJson = new JsonEncoder().convert(sources);
srcRequestRecorder.record("FIX-v1", sourceJson, offset);
_logger.info("About to FIX-v1: ${_hashSource(sourceJson)}");
counter.increment("Fixes");
var response = await analysisServer.getFixesMulti(
sources,
new Location()
..sourceName = sourceName
..offset = offset);
_logger.info('PERF: Computed fixes in ${watch.elapsedMilliseconds}ms.');
return response;
}
Future<FormatResponse> _format(String source, {int offset}) async {
if (source == null) {
throw new BadRequestError('Missing parameter: \'source\'');
}
if (offset == null) offset = 0;
Stopwatch watch = new Stopwatch()..start();
srcRequestRecorder.record("FORMAT", source, offset);
_logger.info("About to FORMAT: ${_hashSource(source)}");
counter.increment("Formats");
// Guard against trying to format code with errors.
AnalysisResults analysisResults = await analyzer.analyze(source);
var response;
if (analysisResults.issues.where((issue) => issue.kind == "error").length >
0) {
response = new FormatResponse(source, offset);
_logger.info('PERF: Format aborted due to analysis errors in'
' ${watch.elapsedMilliseconds}ms.');
return response;
}
response = await analysisServer.format(source, offset);
_logger.info('PERF: Computed format in ${watch.elapsedMilliseconds}ms.');
return response;
}
Future<String> checkCache(String query) => cache.get(query);
Future setCache(String query, String result) =>
cache.set(query, result, expiration: _standardExpiration);
}
String _printCompileProblem(CompilationProblem problem) => problem.message;
String _hashSource(String str) {
return sha1.convert(str.codeUnits).toString();
}