blob: f19f9e000327ca1a56fdd50ee3efdd8811c8b23d [file] [log] [blame]
// Copyright 2014 The Flutter Authors. 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:args/args.dart';
import 'package:file/file.dart';
import 'util.dart';
/// A class to represent a line of input code, with associated line number, file
/// and element name.
class SourceLine {
const SourceLine(
this.text, {
this.file,
this.element,
this.line = -1,
this.startChar = -1,
this.endChar = -1,
this.indent = 0,
});
final File? file;
final String? element;
final int line;
final int startChar;
final int endChar;
final int indent;
final String text;
String toStringWithColumn(int column) =>
'$file:$line:${column + indent}: $text';
SourceLine copyWith({
String? element,
String? text,
File? file,
int? line,
int? startChar,
int? endChar,
int? indent,
}) {
return SourceLine(
text ?? this.text,
element: element ?? this.element,
file: file ?? this.file,
line: line ?? this.line,
startChar: startChar ?? this.startChar,
endChar: endChar ?? this.endChar,
indent: indent ?? this.indent,
);
}
bool get hasFile => file != null;
@override
String toString() => '$file:${line == -1 ? '??' : line}: $text';
}
/// A class containing the name and contents associated with a code block inside of a
/// code sample, for named injection into a template.
class SkeletonInjection {
SkeletonInjection(this.name, this.contents, {this.language = ''});
final String name;
final List<SourceLine> contents;
final String language;
Iterable<String> get stringContents =>
contents.map<String>((SourceLine line) => line.text.trimRight());
String get mergedContent => stringContents.join('\n');
}
/// A base class to represent a block of any kind of sample code, marked by
/// "{@tool (snippet|sample|dartdoc) ...}...{@end-tool}".
abstract class CodeSample {
CodeSample(
this.args,
this.input, {
required this.index,
required SourceLine lineProto,
}) : assert(args.isNotEmpty),
_lineProto = lineProto,
sourceFile = null;
CodeSample.fromFile(
this.args,
this.input,
this.sourceFile, {
required this.index,
required SourceLine lineProto,
}) : assert(args.isNotEmpty),
_lineProto = lineProto;
final File? sourceFile;
final List<String> args;
final List<SourceLine> input;
final SourceLine _lineProto;
String? _sourceFileContents;
String get sourceFileContents {
if (sourceFile != null && _sourceFileContents == null) {
// Strip lines until the first non-comment line. This gets rid of the
// copyright and comment directing the reader to the original source file.
final List<String> stripped = <String>[];
bool doneStrippingHeaders = false;
try {
for (final String line in sourceFile!.readAsLinesSync()) {
if (!doneStrippingHeaders &&
RegExp(r'^\s*(\/\/.*)?$').hasMatch(line)) {
continue;
}
// Stop skipping lines after the first line that isn't stripped.
doneStrippingHeaders = true;
stripped.add(line);
}
} on FileSystemException catch (e) {
throw SnippetException(
'Unable to read linked source file ${sourceFile!}: $e',
file: _lineProto.file?.absolute.path,
);
}
// Remove any section markers
final RegExp sectionMarkerRegExp = RegExp(
r'(\/\/\*\*+\n)?\/\/\* [▼▲]+.*$(\n\/\/\*\*+)?\n\n?',
multiLine: true,
);
_sourceFileContents =
stripped.join('\n').replaceAll(sectionMarkerRegExp, '');
}
return _sourceFileContents ?? '';
}
Iterable<String> get inputStrings =>
input.map<String>((SourceLine line) => line.text);
String get inputAsString => inputStrings.join('\n');
/// The index of this sample within the dartdoc comment it came from.
final int index;
String description = '';
String get element => start.element ?? '';
String output = '';
Map<String, Object?> metadata = <String, Object?>{};
List<SkeletonInjection> parts = <SkeletonInjection>[];
SourceLine get start => input.isEmpty ? _lineProto : input.first;
String get template {
final ArgParser parser = ArgParser();
parser.addOption('template', defaultsTo: '');
final ArgResults parsedArgs = parser.parse(args);
return parsedArgs['template']! as String;
}
@override
String toString() {
final StringBuffer buf = StringBuffer('${args.join(' ')}:\n');
for (final SourceLine line in input) {
buf.writeln(
'${(line.line == -1 ? '??' : line.line).toString().padLeft(4)}: ${line.text} ',
);
}
return buf.toString();
}
String get type;
}
/// A class to represent a snippet of sample code, marked by "{@tool
/// snippet}...{@end-tool}".
///
/// Snippets are code that is not meant to be run as a complete application, but
/// rather as a code usage example.
class SnippetSample extends CodeSample {
SnippetSample(
List<SourceLine> input, {
required int index,
required SourceLine lineProto,
}) : assumptions = <SourceLine>[],
super(
<String>['snippet'],
input,
index: index,
lineProto: lineProto,
);
factory SnippetSample.combine(
List<SnippetSample> sections, {
required int index,
required SourceLine lineProto,
}) {
final List<SourceLine> code =
sections.expand((SnippetSample section) => section.input).toList();
return SnippetSample(code, index: index, lineProto: lineProto);
}
factory SnippetSample.fromStrings(SourceLine firstLine, List<String> code,
{required int index}) {
final List<SourceLine> codeLines = <SourceLine>[];
int startPos = firstLine.startChar;
for (int i = 0; i < code.length; ++i) {
codeLines.add(
firstLine.copyWith(
text: code[i],
line: firstLine.line + i,
startChar: startPos,
),
);
startPos += code[i].length + 1;
}
return SnippetSample(
codeLines,
index: index,
lineProto: firstLine,
);
}
factory SnippetSample.surround(
String prefix,
List<SourceLine> code,
String postfix, {
required int index,
}) {
return SnippetSample(
<SourceLine>[
if (prefix.isNotEmpty) SourceLine(prefix),
...code,
if (postfix.isNotEmpty) SourceLine(postfix),
],
index: index,
lineProto: code.first,
);
}
List<SourceLine> assumptions;
@override
String get template => '';
@override
SourceLine get start =>
input.firstWhere((SourceLine line) => line.file != null);
@override
String get type => 'snippet';
}
/// A class to represent a plain application sample in the dartdoc comments,
/// marked by `{@tool sample ...}...{@end-tool}`.
///
/// Application samples are processed separately from [SnippetSample]s, because
/// they must be injected into templates in order to be analyzed. Each
/// [ApplicationSample] represents one `{@tool sample ...}...{@end-tool}` block
/// in the source file.
class ApplicationSample extends CodeSample {
ApplicationSample({
List<SourceLine> input = const <SourceLine>[],
required List<String> args,
required int index,
required SourceLine lineProto,
}) : assert(args.isNotEmpty),
super(args, input, index: index, lineProto: lineProto);
ApplicationSample.fromFile({
List<SourceLine> input = const <SourceLine>[],
required List<String> args,
required File sourceFile,
required int index,
required SourceLine lineProto,
}) : assert(args.isNotEmpty),
super.fromFile(args, input, sourceFile,
index: index, lineProto: lineProto);
@override
String get type => 'sample';
}
/// A class to represent a Dartpad application sample in the dartdoc comments,
/// marked by `{@tool dartpad ...}...{@end-tool}`.
///
/// Dartpad samples are processed separately from [SnippetSample]s, because they
/// must be injected into templates in order to be analyzed. Each
/// [DartpadSample] represents one `{@tool dartpad ...}...{@end-tool}` block in
/// the source file.
class DartpadSample extends ApplicationSample {
DartpadSample({
super.input,
required super.args,
required super.index,
required super.lineProto,
}) : assert(args.isNotEmpty);
DartpadSample.fromFile({
super.input,
required super.args,
required super.sourceFile,
required super.index,
required super.lineProto,
}) : assert(args.isNotEmpty),
super.fromFile();
@override
String get type => 'dartpad';
}
/// The different types of Dart [SourceElement]s that can be found in a source file.
enum SourceElementType {
/// A class
classType,
/// A field variable of a class.
fieldType,
/// A constructor for a class.
constructorType,
/// A method of a class.
methodType,
/// A function typedef
typedefType,
/// A top level (non-class) variable.
topLevelVariableType,
/// A function, either top level, or embedded in another function.
functionType,
/// An unknown type used for initialization.
unknownType,
}
/// Converts the enum type [SourceElementType] to a human readable string.
String sourceElementTypeAsString(SourceElementType type) {
return switch (type) {
SourceElementType.classType => 'class',
SourceElementType.fieldType => 'field',
SourceElementType.methodType => 'method',
SourceElementType.constructorType => 'constructor',
SourceElementType.typedefType => 'typedef',
SourceElementType.topLevelVariableType => 'variable',
SourceElementType.functionType => 'function',
SourceElementType.unknownType => 'unknown',
};
}
/// A class that represents a Dart element in a source file.
///
/// The element is one of the types in [SourceElementType].
class SourceElement {
/// A factory constructor for SourceElements.
///
/// This uses a factory so that the default for the `comment` and `samples`
/// lists can be modifiable lists.
factory SourceElement(
SourceElementType type,
String name,
int startPos, {
required File file,
String className = '',
List<SourceLine>? comment,
int startLine = -1,
List<CodeSample>? samples,
bool override = false,
}) {
comment ??= <SourceLine>[];
samples ??= <CodeSample>[];
final List<String> commentLines =
comment.map<String>((SourceLine line) => line.text).toList();
final String commentString = commentLines.join('\n');
return SourceElement._(
type,
name,
startPos,
file: file,
className: className,
comment: comment,
startLine: startLine,
samples: samples,
override: override,
commentString: commentString,
commentStringWithoutTools: _getCommentStringWithoutTools(commentString),
commentStringWithoutCode: _getCommentStringWithoutCode(commentString),
commentLines: commentLines,
);
}
const SourceElement._(
this.type,
this.name,
this.startPos, {
required this.file,
this.className = '',
this.comment = const <SourceLine>[],
this.startLine = -1,
this.samples = const <CodeSample>[],
this.override = false,
String commentString = '',
String commentStringWithoutTools = '',
String commentStringWithoutCode = '',
List<String> commentLines = const <String>[],
}) : _commentString = commentString,
_commentStringWithoutTools = commentStringWithoutTools,
_commentStringWithoutCode = commentStringWithoutCode,
_commentLines = commentLines;
final String _commentString;
final String _commentStringWithoutTools;
final String _commentStringWithoutCode;
final List<String> _commentLines;
// Does not include the description of the sample code, just the text outside
// of any dartdoc tools.
static String _getCommentStringWithoutTools(String string) {
return string.replaceAll(
RegExp(r'(\{@tool ([^}]*)\}.*?\{@end-tool\}|/// ?)', dotAll: true), '');
}
// Includes the description text inside of an "@tool"-based sample, but not
// the code itself, or any dartdoc tags.
static String _getCommentStringWithoutCode(String string) {
return string.replaceAll(
RegExp(r'([`]{3}.*?[`]{3}|\{@\w+[^}]*\}|/// ?)', dotAll: true), '');
}
/// The type of the element
final SourceElementType type;
/// The name of the element.
///
/// For example, a method called "doSomething" that is part of the class
/// "MyClass" would have "doSomething" as its name.
final String name;
/// The name of the class the element belongs to, if any.
///
/// This is the empty string if it isn't part of a class.
///
/// For example, a method called "doSomething" that is part of the class
/// "MyClass" would have "MyClass" as its `className`.
final String className;
/// Whether or not this element has the "@override" annotation attached to it.
final bool override;
/// The file that this [SourceElement] was parsed from.
final File file;
/// The character position in the file that this [SourceElement] starts at.
final int startPos;
/// The line in the file that the first position of [SourceElement] is on.
final int startLine;
/// The list of [SourceLine]s that make up the documentation comment for this
/// [SourceElement].
final List<SourceLine> comment;
/// The list of [CodeSample]s that are in the documentation comment for this
/// [SourceElement].
///
/// This field will be populated by calling [replaceSamples].
final List<CodeSample> samples;
/// Get the comments as an iterable of lines.
Iterable<String> get commentLines => _commentLines;
/// Get the comments as a single string.
String get commentString => _commentString;
/// Does not include the description of the sample code, just the text outside of any dartdoc tools.
String get commentStringWithoutTools => _commentStringWithoutTools;
/// Includes the description text inside of an "@tool"-based sample, but not
/// the code itself, or any dartdoc tags.
String get commentStringWithoutCode => _commentStringWithoutCode;
/// The number of samples in the dartdoc comment for this element.
int get sampleCount => samples.length;
/// The number of [DartpadSample]s in the dartdoc comment for this element.
int get dartpadSampleCount => samples.whereType<DartpadSample>().length;
/// The number of [ApplicationSample]s in the dartdoc comment for this element.
int get applicationSampleCount => samples.where((CodeSample sample) {
return sample is ApplicationSample && sample is! DartpadSample;
}).length;
/// The number of [SnippetSample]s in the dartdoc comment for this element.
int get snippetCount => samples.whereType<SnippetSample>().length;
/// Count of comment lines, not including lines of code in the comment.
int get lineCount => commentStringWithoutCode.split('\n').length;
/// Count of comment words, not including words in any code in the comment.
int get wordCount {
return commentStringWithoutCode.split(RegExp(r'\s+')).length;
}
/// Count of comment characters, not including any code samples in the
/// comment, after collapsing each run of whitespace to a single space.
int get charCount =>
commentStringWithoutCode.replaceAll(RegExp(r'\s+'), ' ').length;
/// Whether or not this element's documentation has a "See also:" section in it.
bool get hasSeeAlso => commentStringWithoutTools.contains('See also:');
int get referenceCount {
final RegExp regex = RegExp(r'\[[. \w]*\](?!\(.*\))');
return regex.allMatches(commentStringWithoutCode).length;
}
int get linkCount {
final RegExp regex = RegExp(r'\[[. \w]*\]\(.*\)');
return regex.allMatches(commentStringWithoutCode).length;
}
/// Returns the fully qualified name of this element.
///
/// For example, a method called "doSomething" that is part of the class
/// "MyClass" would have "MyClass.doSomething" as its `elementName`.
String get elementName {
if (type == SourceElementType.constructorType) {
// Constructors already have the name of the class in them.
return name;
}
return className.isEmpty ? name : '$className.$name';
}
/// Returns the type of this element as a [String].
String get typeAsString {
return '${override ? 'overridden ' : ''}${sourceElementTypeAsString(type)}';
}
void replaceSamples(Iterable<CodeSample> samples) {
this.samples.clear();
this.samples.addAll(samples);
}
/// Copy the source element, with some attributes optionally replaced.
SourceElement copyWith({
SourceElementType? type,
String? name,
int? startPos,
File? file,
String? className,
List<SourceLine>? comment,
int? startLine,
List<CodeSample>? samples,
bool? override,
}) {
return SourceElement(
type ?? this.type,
name ?? this.name,
startPos ?? this.startPos,
file: file ?? this.file,
className: className ?? this.className,
comment: comment ?? this.comment,
startLine: startLine ?? this.startLine,
samples: samples ?? this.samples,
override: override ?? this.override,
);
}
}