blob: ea73ad08b83c26a486dd8b5bb2ed53ed3f0eba8f [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 'dart:convert';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/model_utils.dart' as model_utils;
import 'package:meta/meta.dart';
/// Stripped down information derived from [AstNode] containing only information
/// needed to resurrect the source code of [element].
class ModelNode {
final Element _element;
final ResourceProvider _resourceProvider;
final int _sourceEnd;
final int _sourceOffset;
factory ModelNode(
AstNode? sourceNode, Element element, ResourceProvider resourceProvider) {
if (sourceNode == null) {
return ModelNode._(element, resourceProvider,
sourceEnd: -1, sourceOffset: -1);
} else {
// Get a node higher up the syntax tree that includes the semicolon.
// In this case, it is either a [FieldDeclaration] or
// [TopLevelVariableDeclaration]. (#2401)
if (sourceNode is VariableDeclaration) {
sourceNode = sourceNode.parent!.parent!;
assert(sourceNode is FieldDeclaration ||
sourceNode is TopLevelVariableDeclaration);
}
return ModelNode._(element, resourceProvider,
sourceEnd: sourceNode.end, sourceOffset: sourceNode.offset);
}
}
ModelNode._(this._element, this._resourceProvider,
{required int sourceEnd, required int sourceOffset})
: _sourceEnd = sourceEnd,
_sourceOffset = sourceOffset;
bool get _isSynthetic => _sourceEnd < 0 || _sourceOffset < 0;
/// The text of the source code of this node, stripped of the leading
/// indentation, and stripped of the doc comments.
late final String sourceCode = _isSynthetic
? ''
: model_utils
.getFileContentsFor(_element, _resourceProvider)
.substringFromLineStart(_sourceOffset, _sourceEnd)
.stripIndent
.stripDocComments
.trim();
}
@visibleForTesting
extension SourceStringExtensions on String {
String substringFromLineStart(int offset, int endOffset) {
var lineStartOffset = startOfLineWithOffset(offset);
return substring(lineStartOffset, endOffset);
}
// Finds the start of the line which contains the character at [offset].
int startOfLineWithOffset(int offset) {
var i = offset;
// Walk backwards until we find the previous line's EOL character.
while (i > 0) {
i -= 1;
if (this[i] == '\n' || this[i] == '\r') {
i += 1;
break;
}
}
return i;
}
/// Strips leading doc comments from the given source code.
String get stripDocComments {
var remainder = trimLeft();
var lineComments = remainder.startsWith(_tripleSlash) ||
remainder.startsWith(_escapedTripleSlash);
var blockComments = remainder.startsWith(_slashStarStar) ||
remainder.startsWith(_escapedSlashStarStar);
return split('\n').where((String line) {
if (lineComments) {
if (line.startsWith(_tripleSlash) ||
line.startsWith(_escapedTripleSlash)) {
return false;
}
lineComments = false;
return true;
} else if (blockComments) {
if (line.contains(_starSlash) || line.contains(_escapedStarSlash)) {
blockComments = false;
return false;
}
if (line.startsWith(_slashStarStar) ||
line.startsWith(_escapedSlashStarStar)) {
return false;
}
return false;
}
return true;
}).join('\n');
}
/// Strips the common indent from the given source fragment.
String get stripIndent {
var remainder = trimLeft();
var indent = substring(0, length - remainder.length);
return split('\n').map((line) {
line = line.trimRight();
return line.startsWith(indent) ? line.substring(indent.length) : line;
}).join('\n');
}
}
const HtmlEscape _escape = HtmlEscape();
const String _tripleSlash = '///';
final String _escapedTripleSlash = _escape.convert(_tripleSlash);
const String _slashStarStar = '/**';
final String _escapedSlashStarStar = _escape.convert(_slashStarStar);
const String _starSlash = '*/';
final String _escapedStarSlash = _escape.convert(_starSlash);