blob: 0b5d85b1e5a1df4583a939f793402290dd41e164 [file] [log] [blame]
// Copyright (c) 2019, 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:analysis_server/src/edit/nnbd_migration/migration_info.dart';
import 'package:analysis_server/src/edit/nnbd_migration/offset_mapper.dart';
import 'package:analysis_server/src/edit/nnbd_migration/path_mapper.dart';
import 'package:meta/meta.dart';
import 'package:mustache/mustache.dart' as mustache;
import 'package:path/path.dart' as path;
/// A mustache template for one library's instrumentation output.
mustache.Template _createTemplate(String navContent) => mustache.Template(r'''
<html>
<head>
<title>Non-nullable fix instrumentation report</title>
<!-- <script src="{{ highlightJsPath }}"></script>-->
<script>
function highlightTarget() {
var url = document.URL;
var index = url.lastIndexOf("#");
if (index >= 0) {
var name = url.substring(index + 1);
var anchor = document.getElementById(name);
if (anchor != null) {
anchor.className = "target";
}
}
}
</script>
<link rel="stylesheet" href="{{ highlightStylePath }}">
<style>
a:link {
color: #000000;
text-decoration-line: none;
}
a:visited {
color: #000000;
text-decoration-line: none;
}
body {
font-family: sans-serif;
padding: 1em;
}
h2 {
font-size: 1em;
font-weight: bold;
}
.code {
position: absolute;
left: 0.5em;
top: 0.5em;
}
.content {
font-family: monospace;
position: relative;
white-space: pre;
}
.regions {
position: absolute;
left: 0.5em;
top: 0.5em;
/* The content of the regions is not visible; the user instead will see the
* highlighted copy of the content. */
visibility: hidden;
}
.region {
/* Green means this region was added. */
background-color: #ccffcc;
color: #003300;
cursor: default;
display: inline-block;
position: relative;
visibility: visible;
}
.region .tooltip {
background-color: #EEE;
border: solid 2px #999;
color: #333;
cursor: auto;
font-family: sans-serif;
font-size: 0.8em;
left: 0;
margin-left: 0;
padding: 1px;
position: absolute;
top: 100%;
visibility: hidden;
white-space: normal;
width: 200px;
z-index: 1;
}
.region:hover .tooltip {
visibility: visible;
}
.target {
background-color: #FFFFFF;
position: relative;
visibility: visible;
}
</style>
</head>
<body onload="highlightTarget()">
<h1>Non-nullable fix instrumentation report</h1>
<p><em>Well-written introduction to this report.</em></p>
<div class="navigation">
{{# links }}
{{# isLink }}<a href="{{ href }}">{{ name }}</a>{{/ isLink }}
{{^ isLink }}{{ name }}{{/ isLink }}
<br />
{{/ links }}
</div>
{{# units }}'''
'<h2>{{{ path }}}</h2>'
'<div class="content">'
// '<div class="highlighting">'
// '{{! These regions are written out, unmodified, as they need to be found }}'
// '{{! in one simple text string for highlight.js to hightlight them. }}'
// '{{# regions }}'
// '{{ content }}'
// '{{/ regions }}'
// '</div>'
'<div class ="code">'
'{{! The regions are written a second time, but hidden, to include }}'
'{{! anchors. }}' +
navContent +
'</div>'
'<div class="regions">'
'{{! The regions are then written again, overlaying the first two copies }}'
'{{! of the content, to provide tooltips for modified regions. }}'
'{{# regions }}'
'{{^ modified }}{{ content }}{{/ modified }}'
'{{# modified }}<span class="region">{{ content }}'
'<span class="tooltip">{{ explanation }}<ul>'
'{{# details }}'
'<li>'
'<a href="{{ target }}">{{ description }}</a>'
'</li>'
'{{/ details }}</ul></span></span>{{/ modified }}'
'{{/ regions }}'
'</div></div>'
r'''
{{/ units }}
<script lang="javascript">
document.addEventListener("DOMContentLoaded", (event) => {
document.querySelectorAll(".highlighting").forEach((block) => {
hljs.highlightBlock(block);
});
});
</script>
</body>
</html>''');
/// Instrumentation display output for a library that was migrated to use
/// non-nullable types.
class InstrumentationRenderer {
/// Display information for a compilation unit.
final UnitInfo unitInfo;
/// Information for a whole migration, so that libraries can reference each
/// other.
final MigrationInfo migrationInfo;
/// An object used to map the file paths of analyzed files to the file paths
/// of the HTML files used to view the content of those files.
final PathMapper pathMapper;
/// Creates an output object for the given library info.
InstrumentationRenderer(this.unitInfo, this.migrationInfo, this.pathMapper);
/// Builds an HTML view of the instrumentation information in [unitInfo].
String render() {
Map<String, dynamic> mustacheContext = {
'units': <Map<String, dynamic>>[],
'links': migrationInfo.unitLinks(unitInfo),
'highlightJsPath': migrationInfo.highlightJsPath(unitInfo),
'highlightStylePath': migrationInfo.highlightStylePath(unitInfo),
};
mustacheContext['units'].add({
'path': unitInfo.path,
'regions': _computeRegions(unitInfo),
});
String navContent = _computeNavigationContent(unitInfo);
return _createTemplate(navContent).renderString(mustacheContext);
}
/// Return the content of the file with navigation links and anchors added.
String _computeNavigationContent(UnitInfo unitInfo) {
String content = unitInfo.content;
OffsetMapper mapper = unitInfo.offsetMapper;
List<NavigationRegion> regions = []
..addAll(unitInfo.sources ?? <NavigationSource>[])
..addAll(unitInfo.targets);
regions.sort((first, second) {
int offsetComparison = first.offset.compareTo(second.offset);
if (offsetComparison == 0) {
return first is NavigationSource ? -1 : 1;
}
return offsetComparison;
});
StringBuffer navContent = StringBuffer();
int previousOffset = 0;
for (int i = 0; i < regions.length; i++) {
NavigationRegion region = regions[i];
int offset = mapper.map(region.offset);
int length = region.length;
if (offset > previousOffset) {
// Write a non-target region.
navContent.write(content.substring(previousOffset, offset));
if (region is NavigationSource) {
if (i + 1 < regions.length &&
regions[i + 1].offset == region.offset) {
NavigationTarget target = region.target;
if (target == regions[i + 1]) {
// Add a target region. We skip the source because it links to
// itself, which is pointless.
navContent.write('<a id="o${region.offset}">');
navContent.write(content.substring(offset, offset + length));
navContent.write('</a>');
} else {
// Add a source and target region.
// TODO(brianwilkerson) Map the target's file path to the path of
// the corresponding html file. I'd like to do this by adding a
// `FilePathMapper` object so that it can't become inconsistent
// with the code used to decide where to write the html.
String htmlPath = pathMapper.map(target.filePath);
navContent.write('<a id="o${region.offset}" ');
navContent.write('href="$htmlPath#o${target.offset}">');
navContent.write(content.substring(offset, offset + length));
navContent.write('</a>');
}
i++;
} else {
// Add a source region.
NavigationTarget target = region.target;
String htmlPath = pathMapper.map(target.filePath);
navContent.write('<a href="$htmlPath#o${target.offset}">');
navContent.write(content.substring(offset, offset + length));
navContent.write('</a>');
}
} else {
// Add a target region.
navContent.write('<a id="o${region.offset}">');
navContent.write(content.substring(offset, offset + length));
navContent.write('</a>');
}
previousOffset = offset + length;
}
}
if (previousOffset < content.length) {
// Last non-target region.
navContent.write(content.substring(previousOffset));
}
return navContent.toString();
}
/// Return a list of Mustache context, based on the [unitInfo] for both
/// unmodified and modified regions:
///
/// * 'modified': Whether this region represents modified source, or
/// unmodified.
/// * 'content': The textual content of this region.
/// * 'explanation': The Mustache context for the tooltip explaining why the
/// content in this region was modified.
List<Map> _computeRegions(UnitInfo unitInfo) {
String content = unitInfo.content;
List<Map> regions = [];
int previousOffset = 0;
for (var region in unitInfo.regions) {
int offset = region.offset;
int length = region.length;
if (offset > previousOffset) {
// Display a region of unmodified content.
regions.add({
'modified': false,
'content': content.substring(previousOffset, offset),
});
previousOffset = offset + length;
}
List<Map> details = [];
for (var detail in region.details) {
details.add({
'description': detail.description,
'target': _uriForTarget(detail.target),
});
}
regions.add({
'modified': true,
'content': content.substring(offset, offset + length),
'explanation': region.explanation,
'details': details,
});
}
if (previousOffset < content.length) {
// Last region of unmodified content.
regions.add({
'modified': false,
'content': content.substring(previousOffset),
});
}
return regions;
}
/// Return the URL that will navigate to the given [target].
String _uriForTarget(NavigationTarget target) {
path.Context pathContext = migrationInfo.pathContext;
String targetPath = pathContext.setExtension(target.filePath, '.html');
String sourceDir = pathContext.dirname(unitInfo.path);
String relativePath = pathContext.relative(targetPath, from: sourceDir);
return '$relativePath#o${target.offset.toString()}';
}
}
/// A class storing rendering information for an entire migration report.
///
/// This generally provides one [InstrumentationRenderer] (for one library)
/// with information about the rest of the libraries represented in the
/// instrumentation output.
class MigrationInfo {
/// The information about the compilation units that are are migrated.
final List<UnitInfo> units;
/// The resource provider's path context.
final path.Context pathContext;
/// The filesystem root used to create relative paths for each unit.
final String includedRoot;
MigrationInfo(this.units, this.pathContext, this.includedRoot);
/// The path to the highlight.js script, relative to [unitInfo].
String highlightJsPath(UnitInfo unitInfo) =>
pathContext.relative(pathContext.join(includedRoot, 'highlight.pack.js'),
from: pathContext.dirname(unitInfo.path));
/// The path to the highlight.js stylesheet, relative to [unitInfo].
String highlightStylePath(UnitInfo unitInfo) =>
pathContext.relative(pathContext.join(includedRoot, 'androidstudio.css'),
from: pathContext.dirname(unitInfo.path));
/// Generate mustache context for unit links, for navigation in the
/// instrumentation document for [thisUnit].
List<Map<String, Object>> unitLinks(UnitInfo thisUnit) {
return [
for (var unit in units)
{
'name': _computeName(unit),
'isLink': unit != thisUnit,
if (unit != thisUnit) 'href': _pathTo(target: unit, source: thisUnit)
}
];
}
/// Return the path to [unit] from [includedRoot], to be used as a display
/// name for a library.
String _computeName(UnitInfo unit) =>
pathContext.relative(unit.path, from: includedRoot);
/// The path to [target], relative to [from].
String _pathTo({@required UnitInfo target, @required UnitInfo source}) {
String targetPath = pathContext.setExtension(target.path, '.html');
String sourceDir = pathContext.dirname(source.path);
return pathContext.relative(targetPath, from: sourceDir);
}
}