Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(276)

Side by Side Diff: pkg/analysis_server/lib/src/status/get_handler2.dart

Issue 2918553002: Remove dead code related to the diagnostics server. (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 import 'dart:convert';
6 import 'dart:io';
7 import 'dart:math';
8
9 import 'package:analysis_server/protocol/protocol.dart';
10 import 'package:analysis_server/protocol/protocol_generated.dart'
11 hide AnalysisOptions;
12 import 'package:analysis_server/src/analysis_server.dart';
13 import 'package:analysis_server/src/domain_completion.dart';
14 import 'package:analysis_server/src/domain_diagnostic.dart';
15 import 'package:analysis_server/src/domain_execution.dart';
16 import 'package:analysis_server/src/plugin/plugin_manager.dart';
17 import 'package:analysis_server/src/server/http_server.dart';
18 import 'package:analysis_server/src/services/completion/completion_performance.d art';
19 import 'package:analysis_server/src/socket_server.dart';
20 import 'package:analyzer/exception/exception.dart';
21 import 'package:analyzer/file_system/file_system.dart';
22 import 'package:analyzer/instrumentation/instrumentation.dart';
23 import 'package:analyzer/source/error_processor.dart';
24 import 'package:analyzer/source/package_map_resolver.dart';
25 import 'package:analyzer/source/sdk_ext.dart';
26 import 'package:analyzer/src/context/source.dart';
27 import 'package:analyzer/src/dart/analysis/driver.dart';
28 import 'package:analyzer/src/dart/sdk/sdk.dart';
29 import 'package:analyzer/src/generated/engine.dart';
30 import 'package:analyzer/src/generated/sdk.dart';
31 import 'package:analyzer/src/generated/source.dart';
32 import 'package:analyzer/src/generated/utilities_general.dart';
33 import 'package:analyzer/src/services/lint.dart';
34 import 'package:analyzer/task/model.dart';
35 import 'package:path/path.dart' as path;
36 import 'package:plugin/plugin.dart';
37
38 String _writeWithSeparators(int value) {
39 // TODO(devoncarew): Replace with the implementation from package:intl.
40 String str = value.toString();
41 int pos = 3;
42 while (str.length > pos) {
43 int len = str.length;
44 str = '${str.substring(0, len - pos)},${str.substring(len - pos)}';
45 pos += 4;
46 }
47 return str;
48 }
49
50 /**
51 * A function that can be used to generate HTML output into the given [buffer].
52 * The HTML that is generated must be valid (special characters must already be
53 * encoded).
54 */
55 typedef void HtmlGenerator(StringBuffer buffer);
56
57 /**
58 * Instances of the class [GetHandler2] handle GET requests.
59 */
60 class GetHandler2 implements AbstractGetHandler {
61 /**
62 * The path used to request overall performance information.
63 */
64 static const String ANALYSIS_PERFORMANCE_PATH = '/perf/analysis';
65
66 /**
67 * The path used to request code completion information.
68 */
69 static const String COMPLETION_PATH = '/completion';
70
71 /**
72 * The path used to request communication performance information.
73 */
74 static const String COMMUNICATION_PERFORMANCE_PATH = '/perf/communication';
75
76 /**
77 * The path used to request information about a specific context.
78 */
79 static const String CONTEXT_PATH = '/context';
80
81 /**
82 * The path used to request an overlay contents.
83 */
84 static const String OVERLAY_PATH = '/overlay';
85
86 /**
87 * The path used to request overlays information.
88 */
89 static const String OVERLAYS_PATH = '/overlays';
90
91 /**
92 * The path used to request the status of the analysis server as a whole.
93 */
94 static const String STATUS_PATH = '/status';
95
96 /**
97 * Query parameter used to represent the context to search for.
98 */
99 static const String CONTEXT_QUERY_PARAM = 'context';
100
101 /**
102 * Query parameter used to represent the path of an overlayed file.
103 */
104 static const String PATH_PARAM = 'path';
105
106 static final ContentType _htmlContent =
107 new ContentType("text", "html", charset: "utf-8");
108
109 /**
110 * The socket server whose status is to be reported on.
111 */
112 SocketServer _server;
113
114 /**
115 * Buffer containing strings printed by the analysis server.
116 */
117 List<String> _printBuffer;
118
119 /**
120 * Contents of overlay files.
121 */
122 final Map<String, String> _overlayContents = <String, String>{};
123
124 /**
125 * Handler for diagnostics requests.
126 */
127 DiagnosticDomainHandler _diagnosticHandler;
128
129 /**
130 * Initialize a newly created handler for GET requests.
131 */
132 GetHandler2(this._server, this._printBuffer);
133
134 DiagnosticDomainHandler get diagnosticHandler {
135 if (_diagnosticHandler == null) {
136 _diagnosticHandler = new DiagnosticDomainHandler(_server.analysisServer);
137 }
138 return _diagnosticHandler;
139 }
140
141 /**
142 * Return the active [CompletionDomainHandler]
143 * or `null` if either analysis server is not running
144 * or there is no completion domain handler.
145 */
146 CompletionDomainHandler get _completionDomainHandler {
147 AnalysisServer analysisServer = _server.analysisServer;
148 if (analysisServer == null) {
149 return null;
150 }
151 return analysisServer.handlers
152 .firstWhere((h) => h is CompletionDomainHandler, orElse: () => null);
153 }
154
155 /**
156 * Handle a GET request received by the HTTP server.
157 */
158 void handleGetRequest(HttpRequest request) {
159 String path = request.uri.path;
160 if (path == '/') {
161 _returnRedirect(request, STATUS_PATH);
162 } else if (path == STATUS_PATH) {
163 _returnServerStatus(request);
164 } else if (path == ANALYSIS_PERFORMANCE_PATH) {
165 _returnAnalysisPerformance(request);
166 } else if (path == COMPLETION_PATH) {
167 _returnCompletionInfo(request);
168 } else if (path == COMMUNICATION_PERFORMANCE_PATH) {
169 _returnCommunicationPerformance(request);
170 } else if (path == CONTEXT_PATH) {
171 _returnContextInfo(request);
172 } else if (path == OVERLAY_PATH) {
173 _returnOverlayContents(request);
174 } else if (path == OVERLAYS_PATH) {
175 _returnOverlaysInfo(request);
176 } else {
177 _returnUnknownRequest(request);
178 }
179 }
180
181 /**
182 * Return the folder being managed by the given [analysisServer] that matches
183 * the given [contextFilter], or `null` if there is none.
184 */
185 Folder _findFolder(AnalysisServer analysisServer, String contextFilter) {
186 return analysisServer.driverMap.keys.firstWhere(
187 (Folder folder) => folder.path == contextFilter,
188 orElse: () => null);
189 }
190
191 /**
192 * Return `true` if the given analysis [driver] has at least one entry with
193 * an exception.
194 */
195 bool _hasException(AnalysisDriver driver) {
196 // if (driver == null) {
197 // return false;
198 // }
199 // MapIterator<AnalysisTarget, CacheEntry> iterator =
200 // context.analysisCache.iterator();
201 // while (iterator.moveNext()) {
202 // CacheEntry entry = iterator.value;
203 // if (entry == null || entry.exception != null) {
204 // return true;
205 // }
206 // }
207 // TODO(scheglov)
208 return false;
209 }
210
211 /**
212 * Return a response displaying overall performance information.
213 */
214 void _returnAnalysisPerformance(HttpRequest request) {
215 AnalysisServer analysisServer = _server.analysisServer;
216 if (analysisServer == null) {
217 return _returnFailure(request, 'Analysis server is not running');
218 }
219 _writeResponse(request, (StringBuffer buffer) {
220 _writePage(buffer, 'Analysis Performance', [], (StringBuffer buffer) {
221 buffer.write('<h3>Analysis Performance</h3>');
222 _writeTwoColumns(buffer, (StringBuffer buffer) {
223 //
224 // Write performance tags.
225 //
226 buffer.write('<p><b>Performance tag data</b></p>');
227 buffer.write(
228 '<table style="border-collapse: separate; border-spacing: 10px 5px ;">');
229 _writeRow(buffer, ['Time (in ms)', 'Percent', 'Tag name'],
230 header: true);
231 // prepare sorted tags
232 List<PerformanceTag> tags = PerformanceTag.all.toList();
233 tags.remove(ServerPerformanceStatistics.idle);
234 tags.sort((a, b) => b.elapsedMs - a.elapsedMs);
235 // prepare total time
236 int totalTagTime = 0;
237 tags.forEach((PerformanceTag tag) {
238 totalTagTime += tag.elapsedMs;
239 });
240 // write rows
241 void writeRow(PerformanceTag tag) {
242 double percent = (tag.elapsedMs * 100) / totalTagTime;
243 String percentStr = '${percent.toStringAsFixed(2)}%';
244 _writeRow(buffer, [tag.elapsedMs, percentStr, tag.label],
245 classes: ["right", "right", null]);
246 }
247
248 tags.forEach(writeRow);
249 buffer.write('</table>');
250 }, (StringBuffer buffer) {
251 //
252 // Write task model timing information.
253 //
254 buffer.write('<p><b>Task performance data</b></p>');
255 buffer.write(
256 '<table style="border-collapse: separate; border-spacing: 10px 5px ;">');
257 _writeRow(
258 buffer,
259 [
260 'Task Name',
261 'Count',
262 'Total Time (in ms)',
263 'Average Time (in ms)'
264 ],
265 header: true);
266
267 Map<Type, int> countMap = AnalysisTask.countMap;
268 Map<Type, Stopwatch> stopwatchMap = AnalysisTask.stopwatchMap;
269 List<Type> taskClasses = stopwatchMap.keys.toList();
270 taskClasses.sort((Type first, Type second) =>
271 first.toString().compareTo(second.toString()));
272 int totalTaskTime = 0;
273 taskClasses.forEach((Type taskClass) {
274 int count = countMap[taskClass];
275 if (count == null) {
276 count = 0;
277 }
278 int taskTime = stopwatchMap[taskClass].elapsedMilliseconds;
279 totalTaskTime += taskTime;
280 _writeRow(buffer, [
281 taskClass.toString(),
282 count,
283 taskTime,
284 count <= 0 ? '-' : (taskTime / count).toStringAsFixed(3)
285 ], classes: [
286 null,
287 "right",
288 "right",
289 "right"
290 ]);
291 });
292 _writeRow(buffer, ['Total', '-', totalTaskTime, '-'],
293 classes: [null, "right", "right", "right"]);
294 buffer.write('</table>');
295 });
296 });
297 });
298 }
299
300 /**
301 * Return a response displaying overall performance information.
302 */
303 void _returnCommunicationPerformance(HttpRequest request) {
304 AnalysisServer analysisServer = _server.analysisServer;
305 if (analysisServer == null) {
306 return _returnFailure(request, 'Analysis server is not running');
307 }
308 _writeResponse(request, (StringBuffer buffer) {
309 _writePage(buffer, 'Communication Performance', [],
310 (StringBuffer buffer) {
311 buffer.write('<h3>Communication Performance</h3>');
312 _writeTwoColumns(buffer, (StringBuffer buffer) {
313 ServerPerformance perf = analysisServer.performanceDuringStartup;
314 int requestCount = perf.requestCount;
315 num averageLatency = requestCount > 0
316 ? (perf.requestLatency / requestCount).round()
317 : 0;
318 int maximumLatency = perf.maxLatency;
319 num slowRequestPercent = requestCount > 0
320 ? (perf.slowRequestCount * 100 / requestCount).round()
321 : 0;
322 buffer.write('<h4>Startup</h4>');
323 buffer.write('<table>');
324 _writeRow(buffer, [requestCount, 'requests'],
325 classes: ["right", null]);
326 _writeRow(buffer, [averageLatency, 'ms average latency'],
327 classes: ["right", null]);
328 _writeRow(buffer, [maximumLatency, 'ms maximum latency'],
329 classes: ["right", null]);
330 _writeRow(buffer, [slowRequestPercent, '% > 150 ms latency'],
331 classes: ["right", null]);
332 if (analysisServer.performanceAfterStartup != null) {
333 int startupTime = analysisServer.performanceAfterStartup.startTime -
334 perf.startTime;
335 _writeRow(
336 buffer, [startupTime, 'ms for initial analysis to complete']);
337 }
338 buffer.write('</table>');
339 }, (StringBuffer buffer) {
340 ServerPerformance perf = analysisServer.performanceAfterStartup;
341 if (perf == null) {
342 return;
343 }
344 int requestCount = perf.requestCount;
345 num averageLatency = requestCount > 0
346 ? (perf.requestLatency * 10 / requestCount).round() / 10
347 : 0;
348 int maximumLatency = perf.maxLatency;
349 num slowRequestPercent = requestCount > 0
350 ? (perf.slowRequestCount * 100 / requestCount).round()
351 : 0;
352 buffer.write('<h4>Current</h4>');
353 buffer.write('<table>');
354 _writeRow(buffer, [requestCount, 'requests'],
355 classes: ["right", null]);
356 _writeRow(buffer, [averageLatency, 'ms average latency'],
357 classes: ["right", null]);
358 _writeRow(buffer, [maximumLatency, 'ms maximum latency'],
359 classes: ["right", null]);
360 _writeRow(buffer, [slowRequestPercent, '% > 150 ms latency'],
361 classes: ["right", null]);
362 buffer.write('</table>');
363 });
364 });
365 });
366 }
367
368 /**
369 * Return a response displaying code completion information.
370 */
371 void _returnCompletionInfo(HttpRequest request) {
372 String value = request.requestedUri.queryParameters['index'];
373 int index = value != null ? int.parse(value, onError: (_) => 0) : 0;
374 _writeResponse(request, (StringBuffer buffer) {
375 _writePage(buffer, 'Completion Stats', [], (StringBuffer buffer) {
376 _writeCompletionPerformanceDetail(buffer, index);
377 _writeCompletionPerformanceList(buffer);
378 });
379 });
380 }
381
382 /**
383 * Return a response containing information about a single source file in the
384 * cache.
385 */
386 void _returnContextInfo(HttpRequest request) {
387 AnalysisServer analysisServer = _server.analysisServer;
388 if (analysisServer == null) {
389 return _returnFailure(request, 'Analysis server not running');
390 }
391 String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM];
392 if (contextFilter == null) {
393 return _returnFailure(
394 request, 'Query parameter $CONTEXT_QUERY_PARAM required');
395 }
396 AnalysisDriver driver = null;
397 Folder folder = _findFolder(analysisServer, contextFilter);
398 if (folder == null) {
399 return _returnFailure(request, 'Invalid context: $contextFilter');
400 } else {
401 driver = analysisServer.driverMap[folder];
402 }
403
404 List<String> priorityFiles = driver.priorityFiles;
405 List<String> addedFiles = driver.addedFiles.toList();
406 List<String> implicitFiles =
407 driver.knownFiles.difference(driver.addedFiles).toList();
408 addedFiles.sort();
409 implicitFiles.sort();
410
411 // TODO(scheglov) Use file overlays.
412 // _overlayContents.clear();
413 // context.visitContentCache((String fullName, int stamp, String contents) {
414 // _overlayContents[fullName] = contents;
415 // });
416
417 void _writeFiles(StringBuffer buffer, String title, List<String> files) {
418 buffer.write('<h3>$title</h3>');
419 if (files == null || files.isEmpty) {
420 buffer.write('<p>None</p>');
421 } else {
422 buffer.write('<p><table style="width: 100%">');
423 for (String file in files) {
424 buffer.write('<tr><td>');
425 buffer.write(file);
426 buffer.write('</td><td>');
427 if (_overlayContents.containsKey(files)) {
428 buffer.write(makeLink(OVERLAY_PATH, {PATH_PARAM: file}, 'overlay'));
429 }
430 buffer.write('</td></tr>');
431 }
432 buffer.write('</table></p>');
433 }
434 }
435
436 void writeOptions(StringBuffer buffer, AnalysisOptionsImpl options,
437 {void writeAdditionalOptions(StringBuffer buffer)}) {
438 if (options == null) {
439 buffer.write('<p>No option information available.</p>');
440 return;
441 }
442 buffer.write('<p>');
443 _writeOption(
444 buffer, 'Analyze functon bodies', options.analyzeFunctionBodies);
445 _writeOption(buffer, 'Enable asserts in initializer lists',
446 options.enableAssertInitializer);
447 _writeOption(
448 buffer, 'Enable strict call checks', options.enableStrictCallChecks);
449 _writeOption(buffer, 'Enable super mixins', options.enableSuperMixins);
450 _writeOption(buffer, 'Generate dart2js hints', options.dart2jsHint);
451 _writeOption(buffer, 'Generate errors in implicit files',
452 options.generateImplicitErrors);
453 _writeOption(
454 buffer, 'Generate errors in SDK files', options.generateSdkErrors);
455 _writeOption(buffer, 'Generate hints', options.hint);
456 _writeOption(buffer, 'Incremental resolution', options.incremental);
457 _writeOption(buffer, 'Incremental resolution with API changes',
458 options.incrementalApi);
459 _writeOption(buffer, 'Preserve comments', options.preserveComments);
460 _writeOption(buffer, 'Strong mode', options.strongMode);
461 _writeOption(buffer, 'Strong mode hints', options.strongModeHints);
462 if (writeAdditionalOptions != null) {
463 writeAdditionalOptions(buffer);
464 }
465 buffer.write('</p>');
466 }
467
468 _writeResponse(request, (StringBuffer buffer) {
469 String contextName = path.basename(contextFilter);
470 _writePage(buffer, 'Context: $contextName', [contextFilter],
471 (StringBuffer buffer) {
472 buffer.write('<h3>Configuration</h3>');
473
474 AnalysisOptions analysisOptions = driver.analysisOptions;
475
476 _writeColumns(buffer, <HtmlGenerator>[
477 (StringBuffer buffer) {
478 buffer.write('<p><b>Context Options</b></p>');
479 writeOptions(buffer, analysisOptions);
480 },
481 (StringBuffer buffer) {
482 buffer.write('<p><b>SDK Context Options</b></p>');
483 DartSdk sdk = driver?.sourceFactory?.dartSdk;
484 writeOptions(buffer, sdk?.context?.analysisOptions,
485 writeAdditionalOptions: (StringBuffer buffer) {
486 if (sdk is FolderBasedDartSdk) {
487 _writeOption(buffer, 'Use summaries', sdk.useSummary);
488 }
489 });
490 },
491 (StringBuffer buffer) {
492 List<Linter> lints = analysisOptions.lintRules;
493 buffer.write('<p><b>Lints</b></p>');
494 if (lints.isEmpty) {
495 buffer.write('<p>none</p>');
496 } else {
497 for (Linter lint in lints) {
498 buffer.write('<p>');
499 buffer.write(lint.runtimeType);
500 buffer.write('</p>');
501 }
502 }
503
504 List<ErrorProcessor> errorProcessors =
505 analysisOptions.errorProcessors;
506 int processorCount = errorProcessors?.length ?? 0;
507 buffer
508 .write('<p><b>Error Processor count</b>: $processorCount</p>');
509 }
510 ]);
511
512 _writeColumns(buffer, <HtmlGenerator>[
513 (StringBuffer buffer) {
514 buffer.write('<p><b>analysis_options path</b></p>');
515
516 if (driver.contextRoot.optionsFilePath != null) {
517 buffer.write(
518 '<p>${HTML_ESCAPE.convert(driver.contextRoot.optionsFilePath)} </p>');
519 } else {
520 buffer.write('<p>none</p>');
521 }
522 }
523 ]);
524
525 SourceFactory sourceFactory = driver.sourceFactory;
526 if (sourceFactory is SourceFactoryImpl) {
527 buffer.write('<h3>Resolvers</h3>');
528 for (UriResolver resolver in sourceFactory.resolvers) {
529 buffer.write('<p>');
530 buffer.write(resolver.runtimeType);
531 if (resolver is DartUriResolver) {
532 DartSdk sdk = resolver.dartSdk;
533 buffer.write(' (sdk = ');
534 buffer.write(sdk.runtimeType);
535 if (sdk is FolderBasedDartSdk) {
536 buffer.write(' (path = ');
537 buffer.write(sdk.directory.path);
538 buffer.write(')');
539 } else if (sdk is EmbedderSdk) {
540 buffer.write(' (map = ');
541 _writeMap(buffer, sdk.urlMappings);
542 buffer.write(')');
543 }
544 buffer.write(')');
545 } else if (resolver is SdkExtUriResolver) {
546 buffer.write(' (map = ');
547 _writeMap(buffer, resolver.urlMappings);
548 buffer.write(')');
549 } else if (resolver is PackageMapUriResolver) {
550 _writeMap(buffer, resolver.packageMap, valueWriter: _writeList);
551 }
552 buffer.write('</p>');
553 }
554 }
555
556 _writeFiles(
557 buffer, 'Priority Files (${priorityFiles.length})', priorityFiles);
558 _writeFiles(buffer, 'Added Files (${addedFiles.length})', addedFiles);
559 _writeFiles(
560 buffer,
561 'Implicitly Analyzed Files (${implicitFiles.length})',
562 implicitFiles);
563
564 // TODO(scheglov) Show exceptions.
565 // buffer.write('<h3>Exceptions</h3>');
566 // if (exceptions.isEmpty) {
567 // buffer.write('<p>none</p>');
568 // } else {
569 // exceptions.forEach((CaughtException exception) {
570 // _writeException(buffer, exception);
571 // });
572 // }
573 });
574 });
575 }
576
577 void _returnFailure(HttpRequest request, String message) {
578 _writeResponse(request, (StringBuffer buffer) {
579 _writePage(buffer, 'Failure', [], (StringBuffer buffer) {
580 buffer.write(HTML_ESCAPE.convert(message));
581 });
582 });
583 }
584
585 void _returnOverlayContents(HttpRequest request) {
586 String path = request.requestedUri.queryParameters[PATH_PARAM];
587 if (path == null) {
588 return _returnFailure(request, 'Query parameter $PATH_PARAM required');
589 }
590 String contents = _overlayContents[path];
591
592 _writeResponse(request, (StringBuffer buffer) {
593 _writePage(buffer, 'Overlay', [], (StringBuffer buffer) {
594 buffer.write('<pre>${HTML_ESCAPE.convert(contents)}</pre>');
595 });
596 });
597 }
598
599 /**
600 * Return a response displaying overlays information.
601 */
602 void _returnOverlaysInfo(HttpRequest request) {
603 AnalysisServer analysisServer = _server.analysisServer;
604 if (analysisServer == null) {
605 return _returnFailure(request, 'Analysis server is not running');
606 }
607
608 _writeResponse(request, (StringBuffer buffer) {
609 _writePage(buffer, 'Overlay information', [], (StringBuffer buffer) {
610 buffer.write('<table border="1">');
611 _overlayContents.clear();
612 ContentCache overlayState = analysisServer.overlayState;
613 overlayState.accept((String fullName, int stamp, String contents) {
614 buffer.write('<tr>');
615 String link =
616 makeLink(OVERLAY_PATH, {PATH_PARAM: fullName}, fullName);
617 DateTime time = new DateTime.fromMillisecondsSinceEpoch(stamp);
618 _writeRow(buffer, [link, time]);
619 _overlayContents[fullName] = contents;
620 });
621 int count = _overlayContents.length;
622 buffer.write('<tr><td colspan="2">Total: $count entries</td></tr>');
623 buffer.write('</table>');
624 });
625 });
626 }
627
628 void _returnRedirect(HttpRequest request, String pathFragment) {
629 HttpResponse response = request.response;
630 response.redirect(request.uri.resolve(pathFragment));
631 }
632
633 /**
634 * Return a response indicating the status of the analysis server.
635 */
636 void _returnServerStatus(HttpRequest request) {
637 _writeResponse(request, (StringBuffer buffer) {
638 _writePage(buffer, 'Status', [], (StringBuffer buffer) {
639 if (_writeServerStatus(buffer)) {
640 _writeAnalysisStatus(buffer);
641 _writeEditStatus(buffer);
642 _writeExecutionStatus(buffer);
643 _writePluginStatus(buffer);
644 _writeRecentOutput(buffer);
645 }
646 });
647 });
648 }
649
650 /**
651 * Return an error in response to an unrecognized request received by the HTTP
652 * server.
653 */
654 void _returnUnknownRequest(HttpRequest request) {
655 _writeResponse(request, (StringBuffer buffer) {
656 _writePage(buffer, 'Analysis Server', [], (StringBuffer buffer) {
657 buffer.write('<h3>Unknown page: ');
658 buffer.write(request.uri.path);
659 buffer.write('</h3>');
660 buffer.write('''
661 <p>
662 You have reached an un-recognized page. If you reached this page by
663 following a link from a status page, please report the broken link to
664 the Dart analyzer team:
665 <a>https://github.com/dart-lang/sdk/issues/new</a>.
666 </p><p>
667 If you mistyped the URL, you can correct it or return to
668 ${makeLink(STATUS_PATH, {}, 'the main status page')}.
669 </p>''');
670 });
671 });
672 }
673
674 /**
675 * Return a two digit decimal representation of the given non-negative integer
676 * [value].
677 */
678 String _twoDigit(int value) => value.toString().padLeft(2, '0');
679
680 /**
681 * Write the status of the analysis domain (on the main status page) to the
682 * given [buffer] object.
683 */
684 void _writeAnalysisStatus(StringBuffer buffer) {
685 AnalysisServer analysisServer = _server.analysisServer;
686 Map<Folder, AnalysisDriver> driverMap = analysisServer.driverMap;
687 List<Folder> folders = driverMap.keys.toList();
688 folders.sort((Folder first, Folder second) =>
689 first.shortName.compareTo(second.shortName));
690
691 buffer.write('<h3>Analysis Domain</h3>');
692 _writeTwoColumns(buffer, (StringBuffer buffer) {
693 buffer.write('<p>Using package resolver provider: ');
694 buffer.write(_server.packageResolverProvider != null);
695 buffer.write('</p>');
696 buffer.write(makeLink(OVERLAYS_PATH, {}, 'Overlay information'));
697
698 buffer.write('<p><b>Analysis Contexts</b></p>');
699 bool first = true;
700 folders.forEach((Folder folder) {
701 if (first) {
702 first = false;
703 } else {
704 buffer.write('<br>');
705 }
706 String key = folder.shortName;
707 buffer.write(makeLink(CONTEXT_PATH, {CONTEXT_QUERY_PARAM: folder.path},
708 key, _hasException(driverMap[folder])));
709 if (!folder.getChild('.packages').exists) {
710 buffer.write(' [no .packages file]');
711 }
712 });
713
714 int freq = AnalysisServer.performOperationDelayFrequency;
715 String delay = freq > 0 ? '1 ms every $freq ms' : 'off';
716
717 buffer.write('<p><b>Performance Data</b></p>');
718 buffer.write(
719 makeLink(ANALYSIS_PERFORMANCE_PATH, {}, 'Analysis performance'));
720 buffer.write('<br>');
721 buffer.write('Perform operation delay: $delay');
722 buffer.write('<br>');
723 }, (StringBuffer buffer) {
724 _writeSubscriptionMap(
725 buffer, AnalysisService.VALUES, analysisServer.analysisServices);
726 });
727 }
728
729 /**
730 * Write multiple columns of information to the given [buffer], where the list
731 * of [columns] functions are used to generate the content of those columns.
732 */
733 void _writeColumns(StringBuffer buffer, List<HtmlGenerator> columns) {
734 buffer
735 .write('<table class="column"><tr class="column"><td class="column">');
736 int count = columns.length;
737 for (int i = 0; i < count; i++) {
738 if (i > 0) {
739 buffer.write('</td><td class="column">');
740 }
741 columns[i](buffer);
742 }
743 buffer.write('</td></tr></table>');
744 }
745
746 /**
747 * Write performance information about a specific completion request
748 * to the given [buffer] object.
749 */
750 void _writeCompletionPerformanceDetail(StringBuffer buffer, int index) {
751 CompletionDomainHandler handler = _completionDomainHandler;
752 CompletionPerformance performance;
753 if (handler != null) {
754 List<CompletionPerformance> list = handler.performanceList;
755 if (list != null && list.isNotEmpty) {
756 performance = list[max(0, min(list.length - 1, index))];
757 }
758 }
759 if (performance == null) {
760 buffer.write('<h3>Completion Performance Detail</h3>');
761 buffer.write('<p>No completions yet</p>');
762 return;
763 }
764 buffer.write('<h3>Completion Performance Detail</h3>');
765 buffer.write('<p>${performance.startTimeAndMs} for ${performance.source}');
766 buffer.write('<table>');
767 _writeRow(buffer, ['Elapsed', '', 'Operation'], header: true);
768 performance.operations.forEach((OperationPerformance op) {
769 String elapsed = op.elapsed != null ? op.elapsed.toString() : '???';
770 _writeRow(buffer, [elapsed, '&nbsp;&nbsp;', op.name]);
771 });
772 buffer.write('</table>');
773 buffer.write('<p><b>Compute Cache Performance</b>: ');
774 if (handler.computeCachePerformance == null) {
775 buffer.write('none');
776 } else {
777 int elapsed = handler.computeCachePerformance.elapsedInMilliseconds;
778 Source source = handler.computeCachePerformance.source;
779 buffer.write(' $elapsed ms for $source');
780 }
781 buffer.write('</p>');
782 }
783
784 /**
785 * Write a table showing summary information for the last several
786 * completion requests to the given [buffer] object.
787 */
788 void _writeCompletionPerformanceList(StringBuffer buffer) {
789 CompletionDomainHandler handler = _completionDomainHandler;
790 buffer.write('<h3>Completion Performance List</h3>');
791 if (handler == null) {
792 return;
793 }
794 buffer.write('<table>');
795 _writeRow(
796 buffer,
797 [
798 'Start Time',
799 '',
800 'First (ms)',
801 '',
802 'Complete (ms)',
803 '',
804 '# Notifications',
805 '',
806 '# Suggestions',
807 '',
808 'Snippet'
809 ],
810 header: true);
811 int index = 0;
812 for (CompletionPerformance performance in handler.performanceList) {
813 String link = makeLink(COMPLETION_PATH, {'index': '$index'},
814 '${performance.startTimeAndMs}');
815 _writeRow(buffer, [
816 link,
817 '&nbsp;&nbsp;',
818 performance.firstNotificationInMilliseconds,
819 '&nbsp;&nbsp;',
820 performance.elapsedInMilliseconds,
821 '&nbsp;&nbsp;',
822 performance.notificationCount,
823 '&nbsp;&nbsp;',
824 performance.suggestionCount,
825 '&nbsp;&nbsp;',
826 HTML_ESCAPE.convert(performance.snippet)
827 ]);
828 ++index;
829 }
830
831 buffer.write('</table>');
832 buffer.write('''
833 <p><strong>First (ms)</strong> - the number of milliseconds
834 from when completion received the request until the first notification
835 with completion results was queued for sending back to the client.
836 <p><strong>Complete (ms)</strong> - the number of milliseconds
837 from when completion received the request until the final notification
838 with completion results was queued for sending back to the client.
839 <p><strong># Notifications</strong> - the total number of notifications
840 sent to the client with completion results for this request.
841 <p><strong># Suggestions</strong> - the number of suggestions
842 sent to the client in the first notification, followed by a comma,
843 followed by the number of suggestions send to the client
844 in the last notification. If there is only one notification,
845 then there will be only one number in this column.''');
846 }
847
848 /**
849 * Write the status of the edit domain (on the main status page) to the given
850 * [buffer].
851 */
852 void _writeEditStatus(StringBuffer buffer) {
853 buffer.write('<h3>Edit Domain</h3>');
854 _writeTwoColumns(buffer, (StringBuffer buffer) {
855 buffer.write(makeLink(COMPLETION_PATH, {}, 'Completion stats'));
856 }, (StringBuffer buffer) {});
857 }
858
859 /**
860 * Write a representation of the given [caughtException] to the given
861 * [buffer]. If [isCause] is `true`, then the exception was a cause for
862 * another exception.
863 */
864 void _writeException(StringBuffer buffer, CaughtException caughtException,
865 {bool isCause: false}) {
866 Object exception = caughtException.exception;
867
868 if (exception is AnalysisException) {
869 buffer.write('<p>');
870 if (isCause) {
871 buffer.write('Caused by ');
872 }
873 buffer.write(exception.message);
874 buffer.write('</p>');
875 _writeStackTrace(buffer, caughtException.stackTrace);
876 CaughtException cause = exception.cause;
877 if (cause != null) {
878 buffer.write('<blockquote>');
879 _writeException(buffer, cause, isCause: true);
880 buffer.write('</blockquote>');
881 }
882 } else {
883 buffer.write('<p>');
884 if (isCause) {
885 buffer.write('Caused by ');
886 }
887 buffer.write(exception.toString());
888 buffer.write('<p>');
889 _writeStackTrace(buffer, caughtException.stackTrace);
890 }
891 }
892
893 /**
894 * Write the status of the execution domain (on the main status page) to the
895 * given [buffer].
896 */
897 void _writeExecutionStatus(StringBuffer buffer) {
898 AnalysisServer analysisServer = _server.analysisServer;
899 ExecutionDomainHandler handler = analysisServer.handlers.firstWhere(
900 (RequestHandler handler) => handler is ExecutionDomainHandler,
901 orElse: () => null);
902 Set<ExecutionService> services = new Set<ExecutionService>();
903 if (handler.onFileAnalyzed != null) {
904 services.add(ExecutionService.LAUNCH_DATA);
905 }
906
907 if (handler != null) {
908 buffer.write('<h3>Execution Domain</h3>');
909 _writeTwoColumns(buffer, (StringBuffer buffer) {
910 buffer.write('<br>');
911 }, (StringBuffer buffer) {
912 _writeSubscriptionList(buffer, ExecutionService.VALUES, services);
913 });
914 }
915 }
916
917 /**
918 * Write to the given [buffer] a representation of the given [list].
919 */
920 void _writeList<E>(StringBuffer buffer, List<E> list,
921 {void elementWriter(StringBuffer buffer, E element)}) {
922 buffer.write('[');
923 for (int i = 0; i < list.length; i++) {
924 if (i > 0) {
925 buffer.write(', ');
926 }
927 if (elementWriter == null) {
928 buffer.write(list[i]);
929 } else {
930 elementWriter(buffer, list[i]);
931 }
932 }
933 buffer.write(']');
934 }
935
936 /**
937 * Write to the given [buffer] a representation of the given [map].
938 */
939 void _writeMap<V>(StringBuffer buffer, Map<String, V> map,
940 {void valueWriter(StringBuffer buffer, V value)}) {
941 List<String> keys = map.keys.toList();
942 keys.sort();
943 int length = keys.length;
944 buffer.write('{');
945 for (int i = 0; i < length; i++) {
946 buffer.write('<br>');
947 String key = keys[i];
948 V value = map[key];
949 if (i > 0) {
950 buffer.write(', ');
951 }
952 buffer.write(key);
953 buffer.write(' = ');
954 if (valueWriter == null) {
955 buffer.write(value);
956 } else {
957 valueWriter(buffer, value);
958 }
959 }
960 buffer.write('<br>}');
961 }
962
963 /**
964 * Write a representation of an analysis option with the given [name] and
965 * [value] to the given [buffer]. The option should be separated from other
966 * options unless the [last] flag is true, indicating that this is the last
967 * option in the list of options.
968 */
969 void _writeOption(StringBuffer buffer, String name, Object value,
970 {bool last: false}) {
971 buffer.write(name);
972 buffer.write(' = ');
973 buffer.write(value.toString());
974 if (!last) {
975 buffer.write('<br>');
976 }
977 }
978
979 /**
980 * Write a standard HTML page to the given [buffer]. The page will have the
981 * given [title] and a body that is generated by the given [body] generator.
982 */
983 void _writePage(StringBuffer buffer, String title, List<String> subtitles,
984 HtmlGenerator body) {
985 DateTime now = new DateTime.now();
986 String date = "${now.month}/${now.day}/${now.year}";
987 String time =
988 "${now.hour}:${_twoDigit(now.minute)}:${_twoDigit(now.second)}.${now.mil lisecond}";
989
990 buffer.write('<!DOCTYPE html>');
991 buffer.write('<html>');
992 buffer.write('<head>');
993 buffer.write('<meta charset="utf-8">');
994 buffer.write(
995 '<meta name="viewport" content="width=device-width, initial-scale=1.0">' );
996 buffer.write('<title>Analysis Server</title>');
997 buffer.write('<style>');
998 buffer.write('a {color: #0000DD; text-decoration: none;}');
999 buffer.write('a:link.error {background-color: #FFEEEE;}');
1000 buffer.write('a:visited.error {background-color: #FFEEEE;}');
1001 buffer.write('a:hover.error {background-color: #FFEEEE;}');
1002 buffer.write('a:active.error {background-color: #FFEEEE;}');
1003 buffer.write(
1004 'div.subtitle {float: right; font-weight: normal; font-size: 1rem;}');
1005 buffer.write('h2 {margin-top: 0;}');
1006 buffer.write('h3 {border-bottom: 1px #DDD solid; margin-bottom: 0em;}');
1007 buffer.write('p {margin-top: 0.5em; margin-bottom: 0;}');
1008 buffer.write(
1009 'p.commentary {margin-top: 1em; margin-bottom: 1em; margin-left: 2em; fo nt-style: italic;}');
1010 // response.write('span.error {text-decoration-line: underline; text-decorati on-color: red; text-decoration-style: wavy;}');
1011 buffer.write(
1012 'table.column {border: 0px solid black; width: 100%; table-layout: fixed ;}');
1013 buffer.write('td.column {vertical-align: top; width: 50%;}');
1014 buffer.write('td.right {text-align: right;}');
1015 buffer.write('th {text-align: left; vertical-align:top;}');
1016 buffer.write('tr {vertical-align:top;}');
1017 buffer.write('</style>');
1018 buffer.write('</head>');
1019
1020 buffer.write('<body>');
1021 buffer.write('<h2>$title <div class="subtitle">$date, $time</div></h2>');
1022 if (subtitles != null && subtitles.isNotEmpty) {
1023 bool first = true;
1024 for (String subtitle in subtitles) {
1025 if (first) {
1026 first = false;
1027 } else {
1028 buffer.write('<br>');
1029 }
1030 buffer.write('<b>');
1031 buffer.write(subtitle);
1032 buffer.write('</b>');
1033 }
1034 }
1035 try {
1036 body(buffer);
1037 } catch (exception, stackTrace) {
1038 buffer.write('<h3>Exception while creating page</h3>');
1039 _writeException(buffer, new CaughtException(exception, stackTrace));
1040 }
1041 buffer.write('</body>');
1042 buffer.write('</html>');
1043 }
1044
1045 /**
1046 * Write the recent output section (on the main status page) to the given
1047 * [buffer] object.
1048 */
1049 void _writePluginStatus(StringBuffer buffer) {
1050 void writePlugin(Plugin plugin) {
1051 buffer.write(plugin.uniqueIdentifier);
1052 buffer.write(' (');
1053 buffer.write(plugin.runtimeType);
1054 buffer.write(')<br>');
1055 }
1056
1057 buffer.write('<h3>Plugin Status</h3><p>');
1058 writePlugin(AnalysisEngine.instance.enginePlugin);
1059 writePlugin(_server.serverPlugin);
1060 for (Plugin plugin in _server.analysisServer.userDefinedPlugins) {
1061 writePlugin(plugin);
1062 }
1063 buffer.write('<p>');
1064 }
1065
1066 /**
1067 * Write the recent output section (on the main status page) to the given
1068 * [buffer] object.
1069 */
1070 void _writeRecentOutput(StringBuffer buffer) {
1071 buffer.write('<h3>Recent Output</h3>');
1072 String output = HTML_ESCAPE.convert(_printBuffer.join('\n'));
1073 if (output.isEmpty) {
1074 buffer.write('<i>none</i>');
1075 } else {
1076 buffer.write('<pre>');
1077 buffer.write(output);
1078 buffer.write('</pre>');
1079 }
1080 }
1081
1082 void _writeResponse(HttpRequest request, HtmlGenerator writePage) {
1083 HttpResponse response = request.response;
1084 response.statusCode = HttpStatus.OK;
1085 response.headers.contentType = _htmlContent;
1086 try {
1087 StringBuffer buffer = new StringBuffer();
1088 try {
1089 writePage(buffer);
1090 } catch (exception, stackTrace) {
1091 buffer.clear();
1092 _writePage(buffer, 'Internal Exception', [], (StringBuffer buffer) {
1093 _writeException(buffer, new CaughtException(exception, stackTrace));
1094 });
1095 }
1096 response.write(buffer.toString());
1097 } finally {
1098 response.close();
1099 }
1100 }
1101
1102 /**
1103 * Write a single row within a table to the given [buffer]. The row will have
1104 * one cell for each of the [columns], and will be a header row if [header] is
1105 * `true`.
1106 */
1107 void _writeRow(StringBuffer buffer, List<Object> columns,
1108 {bool header: false, List<String> classes}) {
1109 buffer.write('<tr>');
1110 int count = columns.length;
1111 int maxClassIndex = classes == null ? 0 : classes.length - 1;
1112 for (int i = 0; i < count; i++) {
1113 String classAttribute = '';
1114 if (classes != null) {
1115 String className = classes[min(i, maxClassIndex)];
1116 if (className != null) {
1117 classAttribute = ' class="$className"';
1118 }
1119 }
1120 if (header) {
1121 buffer.write('<th$classAttribute>');
1122 } else {
1123 buffer.write('<td$classAttribute>');
1124 }
1125 if (columns[i] is int) {
1126 buffer.write(_writeWithSeparators(columns[i]));
1127 } else {
1128 buffer.write(columns[i]);
1129 }
1130 if (header) {
1131 buffer.write('</th>');
1132 } else {
1133 buffer.write('</td>');
1134 }
1135 }
1136 buffer.write('</tr>');
1137 }
1138
1139 /**
1140 * Write the status of the service domain (on the main status page) to the
1141 * given [response] object.
1142 */
1143 bool _writeServerStatus(StringBuffer buffer) {
1144 AnalysisServer analysisServer = _server.analysisServer;
1145 Set<ServerService> services = analysisServer.serverServices;
1146
1147 buffer.write('<h3>Server Domain</h3>');
1148 _writeTwoColumns(buffer, (StringBuffer buffer) {
1149 buffer.write('<p><b>State</b></p>');
1150 if (analysisServer == null) {
1151 buffer.write('Status: <span style="color:red">Not running</span>');
1152 return;
1153 }
1154 buffer.write('Status: Running<br>');
1155 buffer.write('New analysis driver: ');
1156 buffer.write(analysisServer.options.enableNewAnalysisDriver);
1157 buffer.write('<br>');
1158 buffer.write('Instrumentation: ');
1159 if (AnalysisEngine.instance.instrumentationService.isActive) {
1160 buffer.write('<strong>active</strong>');
1161 } else {
1162 buffer.write('inactive');
1163 }
1164 buffer.write('<br>');
1165 buffer.write('Process ID: ');
1166 buffer.write(pid);
1167
1168 buffer.write('<p><b>Performance Data</b></p>');
1169 buffer.write(makeLink(
1170 COMMUNICATION_PERFORMANCE_PATH, {}, 'Communication performance'));
1171
1172 if (AnalysisEngine.instance.instrumentationService.isActive) {
1173 buffer.write('<p><b>Instrumentation</b></p>');
1174 InstrumentationServer instrumentationServer = AnalysisEngine
1175 .instance.instrumentationService.instrumentationServer;
1176 String description = instrumentationServer.describe;
1177 HtmlEscape htmlEscape = new HtmlEscape(HtmlEscapeMode.ELEMENT);
1178 description = htmlEscape.convert(description);
1179 // Convert http(s): references to hyperlinks.
1180 final RegExp urlRegExp = new RegExp(r'[http|https]+:\/*(\S+)');
1181 description = description.replaceAllMapped(urlRegExp, (Match match) {
1182 return '<a href="${match.group(0)}">${match.group(1)}</a>';
1183 });
1184 buffer.write(description.replaceAll('\n', '<br>'));
1185 }
1186 }, (StringBuffer buffer) {
1187 _writeSubscriptionList(buffer, ServerService.VALUES, services);
1188 buffer.write('<p><b>Versions</b></p>');
1189 buffer.write('Dart SDK: ${Platform.version}<br>');
1190 buffer.write('Analysis server version: ${AnalysisServer.VERSION}<br>');
1191 buffer.write('<p><b>Plugins</b></p>');
1192 List<PluginInfo> plugins = analysisServer.pluginManager.plugins;
1193 if (plugins.isEmpty) {
1194 buffer.write('none<br>');
1195 } else {
1196 plugins.sort(
1197 (first, second) => first.data.name.compareTo(second.data.name));
1198 for (PluginInfo plugin in plugins) {
1199 buffer.write(plugin.data.name);
1200 buffer.write(' (');
1201 buffer.write(plugin.data.pluginId);
1202 buffer.write(', ');
1203 buffer.write(plugin.data.version);
1204 buffer.write(')<br>');
1205 }
1206 }
1207 });
1208 return analysisServer != null;
1209 }
1210
1211 /**
1212 * Write a representation of the given [stackTrace] to the given [buffer].
1213 */
1214 void _writeStackTrace(StringBuffer buffer, StackTrace stackTrace) {
1215 if (stackTrace != null) {
1216 String trace = stackTrace.toString().replaceAll('#', '<br>#');
1217 if (trace.startsWith('<br>#')) {
1218 trace = trace.substring(4);
1219 }
1220 buffer.write('<p>');
1221 buffer.write(trace);
1222 buffer.write('</p>');
1223 }
1224 }
1225
1226 /**
1227 * Given a [service] that could be subscribed to and a set of the services
1228 * that are actually subscribed to ([subscribedServices]), write a
1229 * representation of the service to the given [buffer].
1230 */
1231 void _writeSubscriptionInList(
1232 StringBuffer buffer, Enum service, Set<Enum> subscribedServices) {
1233 if (subscribedServices.contains(service)) {
1234 buffer.write('<code>+ </code>');
1235 } else {
1236 buffer.write('<code>- </code>');
1237 }
1238 buffer.write(service.name);
1239 buffer.write('<br>');
1240 }
1241
1242 /**
1243 * Given a [service] that could be subscribed to and a set of paths that are
1244 * subscribed to the services ([subscribedPaths]), write a representation of
1245 * the service to the given [buffer].
1246 */
1247 void _writeSubscriptionInMap(
1248 StringBuffer buffer, Enum service, Set<String> subscribedPaths) {
1249 buffer.write('<p>');
1250 buffer.write(service.name);
1251 buffer.write('<br>');
1252 if (subscribedPaths != null && subscribedPaths.isNotEmpty) {
1253 List<String> paths = subscribedPaths.toList();
1254 paths.sort();
1255 for (String path in paths) {
1256 buffer.write('$path<br>');
1257 }
1258 }
1259 buffer.write('</p>');
1260 }
1261
1262 /**
1263 * Given a list containing all of the services that can be subscribed to in a
1264 * single domain ([allServices]) and a set of the services that are actually
1265 * subscribed to ([subscribedServices]), write a representation of the
1266 * subscriptions to the given [buffer].
1267 */
1268 void _writeSubscriptionList(StringBuffer buffer, List<Enum> allServices,
1269 Set<Enum> subscribedServices) {
1270 buffer.write('<p><b>Subscriptions</b></p>');
1271 for (Enum service in allServices) {
1272 _writeSubscriptionInList(buffer, service, subscribedServices);
1273 buffer.write('<br>');
1274 }
1275 }
1276
1277 /**
1278 * Given a list containing all of the services that can be subscribed to in a
1279 * single domain ([allServices]) and a set of the services that are actually
1280 * subscribed to ([subscribedServices]), write a representation of the
1281 * subscriptions to the given [buffer].
1282 */
1283 void _writeSubscriptionMap(StringBuffer buffer, List<Enum> allServices,
1284 Map<Enum, Set<String>> subscribedServices) {
1285 buffer.write('<p><b>Subscriptions</b></p>');
1286 for (Enum service in allServices) {
1287 _writeSubscriptionInMap(buffer, service, subscribedServices[service]);
1288 }
1289 }
1290
1291 /**
1292 * Write two columns of information to the given [buffer], where the
1293 * [leftColumn] and [rightColumn] functions are used to generate the content
1294 * of those columns.
1295 */
1296 void _writeTwoColumns(StringBuffer buffer, HtmlGenerator leftColumn,
1297 HtmlGenerator rightColumn) {
1298 buffer
1299 .write('<table class="column"><tr class="column"><td class="column">');
1300 leftColumn(buffer);
1301 buffer.write('</td><td class="column">');
1302 rightColumn(buffer);
1303 buffer.write('</td></tr></table>');
1304 }
1305
1306 /**
1307 * Create a link to [path] with query parameters [params], with inner HTML
1308 * [innerHtml]. If [hasError] is `true`, then the link will have the class
1309 * 'error'.
1310 */
1311 static String makeLink(
1312 String path, Map<String, String> params, String innerHtml,
1313 [bool hasError = false]) {
1314 Uri uri = params.isEmpty
1315 ? new Uri(path: path)
1316 : new Uri(path: path, queryParameters: params);
1317 String href = HTML_ESCAPE.convert(uri.toString());
1318 String classAttribute = hasError ? ' class="error"' : '';
1319 return '<a href="$href" $classAttribute>$innerHtml</a>';
1320 }
1321 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698