Skip to content

Commit

Permalink
update dartdoc_test (#239)
Browse files Browse the repository at this point in the history
* add more examples and fix extracting

* code splitting and print code samples and it's location.

* set overlay of sample code and update dart sdk
  • Loading branch information
takumma authored Jun 3, 2024
1 parent 356c182 commit 5ca5135
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 148 deletions.
6 changes: 6 additions & 0 deletions dartdoc_test/bin/analyze.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:dartdoc_test/dartdoc_test.dart';

Future<void> main(List<String> args) async {
final dartdocTest = DartDocTest();
dartdocTest.runAnalyze();
}
164 changes: 17 additions & 147 deletions dartdoc_test/bin/dartdoc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,153 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';
import 'dart:io';

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'package:markdown/markdown.dart';

Future<void> main() async {
final rootFolder = Directory.current.absolute.path;
final exampleFolder = p.join(rootFolder, 'example');
final contextCollection = AnalysisContextCollection(
includedPaths: [exampleFolder],
resourceProvider: PhysicalResourceProvider.INSTANCE,
import 'package:args/args.dart';
import 'package:dartdoc_test/dartdoc_test.dart';

final _parser = ArgParser()
..addFlag(
'help',
abbr: 'h',
help: 'Show help',
negatable: false,
)
..addMultiOption(
'directory',
abbr: 'd',
help: 'Directories to analyze',
);

final ctx = contextCollection.contextFor(exampleFolder);

final session = ctx.currentSession;
//session.analysisContext.contextRoot.analyzedFiles(); // Filter for .dart files
final target = p.join(exampleFolder, 'main.dart');
final u = session.getParsedUnit(target);
if (u is ParsedUnitResult) {
final comments = extractDocumentationComments(u);
print(comments.length);
for (final c in comments) {
extractCodeSamples(c);
}
}
}

class DocumentationComment {
final FileSpan span;
final String contents;

DocumentationComment({
required this.contents,
required this.span,
});
}

List<DocumentationComment> extractDocumentationComments(ParsedUnitResult r) {
final file = SourceFile.fromString(r.content, url: r.uri);
final comments = <DocumentationComment>[];
r.unit.accept(_ForEachCommentAstVisitor((comment) {
if (comment.isDocumentation) {
final span = file.span(comment.offset, comment.end);
var lines = LineSplitter.split(span.text);
if (!lines.every((line) => line.startsWith('///'))) {
// TODO: Consider supporting comments using /** */
return; // ignore comments not using ///
}
lines = lines.map((line) => line.substring(3));
if (lines.every((line) => line.isEmpty || line.startsWith(' '))) {
lines = lines.map((line) => line.isEmpty ? '' : line.substring(1));
}
comments.add(DocumentationComment(
contents: lines.join('\n'),
span: span,
));
}
}));
return comments;
}

class _ForEachCommentAstVisitor extends RecursiveAstVisitor<void> {
final void Function(Comment comment) _forEach;

_ForEachCommentAstVisitor(this._forEach);

@override
void visitComment(Comment node) => _forEach(node);
}

class DocumentationCodeSample {
final DocumentationComment comment;
final String code;
// TODO: Find the SourceSpan of [code] within [comment], this is pretty hard
// to do because package:markdown doesn't provide any line-numbers or
// offsets. One option is to parse it manually, instead of using
// package:markdown. Or just search for ```dart and ``` and use that to
// find code samples.

DocumentationCodeSample({
required this.comment,
required this.code,
});
}

final _md = Document(extensionSet: ExtensionSet.gitHubWeb);

List<DocumentationCodeSample> extractCodeSamples(DocumentationComment comment) {
final samples = <DocumentationCodeSample>[];
final nodes = _md.parse(comment.contents);
nodes.accept(_ForEachElement((element) {
if (element.tag == 'code' &&
element.attributes['class'] == 'language-dart') {
var code = '';
element.children?.accept(_ForEachText((text) {
code += text.textContent;
}));
if (code.isNotEmpty) {
samples.add(DocumentationCodeSample(comment: comment, code: code));
}
}
}));
return samples;
}

extension on List<Node> {
void accept(NodeVisitor visitor) {
for (final node in this) {
node.accept(visitor);
}
}
}

class _ForEachElement extends NodeVisitor {
final void Function(Element element) _forEach;

_ForEachElement(this._forEach);

@override
bool visitElementBefore(Element element) => true;

@override
void visitElementAfter(Element element) => _forEach(element);

@override
void visitText(Text text) {}
}

class _ForEachText extends NodeVisitor {
final void Function(Text element) _forEach;

_ForEachText(this._forEach);

@override
bool visitElementBefore(Element element) => true;

@override
void visitElementAfter(Element element) {}

@override
void visitText(Text text) => _forEach(text);
Future<void> main(List<String> args) async {
final dartdocTest = DartDocTest();
dartdocTest.run();
}
106 changes: 106 additions & 0 deletions dartdoc_test/example/example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/// Example of documentation comments and code samples in Dart.
/// This is a simple class example.
///
/// This class demonstrates a simple Dart class with a single method.
class SimpleClass {
/// Adds two numbers together.
///
/// The method takes two integers [a] and [b], and returns their sum.
///
/// Example:
/// ```
/// final result = SimpleClass().add(2, 3);
/// print(result); // 5
/// ```
int add(int a, int b) {
return a + b;
}
}

/// This class demonstrates more complex documentation comments,
/// including parameter descriptions and a detailed method explanation.
class ComplexClass {
/// Multiplies two numbers together.
///
/// Takes two integers [x] and [y], multiplies them and returns the result.
/// This method handles large integers and ensures no overflow occurs.
///
/// Example:
/// ```dart
/// final product = ComplexClass().multiply(4, 5);
/// print(product); // 20
/// ```
int multiply(int x, int y) {
return x * y;
}

/// Calculates the factorial of a number.
///
/// This method uses a recursive approach to calculate the factorial of [n].
/// It throws an [ArgumentError] if [n] is negative.
///
/// Example:
/// ```dart
/// final fact = ComplexClass().factorial(5);
/// print(fact); // 120
/// ```
///
/// Throws:
/// - [ArgumentError] if [n] is negative.
int factorial(int n) {
if (n < 0) throw ArgumentError('Negative numbers are not allowed.');
return n == 0 ? 1 : n * factorial(n - 1);
}
}

/// Checks if a string is a palindrome.
///
/// This method ignores case and non-alphanumeric characters.
///
/// Example:
/// ```dart
/// final isPalindrome = Utility.isPalindrome('A man, a plan, a canal, Panama');
/// print(isPalindrome); // true
/// ```
bool isPalindrome(String s) {
var sanitized = s.replaceAll(RegExp(r'[^A-Za-z0-9]'), '').toLowerCase();
return sanitized == sanitized.split('').reversed.join('');
}

/// Calculates the greatest common divisor (GCD) of two numbers.
///
/// Uses the Euclidean algorithm to find the GCD of [a] and [b].
///
/// Example1:
/// ```dart
/// final gcd = Utility.gcd(48, 18);
/// print(gcd); // 6
/// ```
int gcd(int a, int b) {
while (b != 0) {
var t = b;
b = a % b;
a = t;
}
return a;
}

void main() {
// Test cases to validate the functionality and documentation of the methods.

// SimpleClass tests
final simple = SimpleClass();
assert(simple.add(2, 3) == 5);

// ComplexClass tests
final complex = ComplexClass();
assert(complex.multiply(4, 5) == 20);
assert(complex.factorial(5) == 120);

// Utility tests
assert(isPalindrome('A man, a plan, a canal, Panama'));
assert(gcd(48, 18) == 6);

print('All tests passed!');
}
11 changes: 11 additions & 0 deletions dartdoc_test/example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ void sayHello() {
// My comment
print('Hello world!');
}

/// Simple function that prints `"Hello world!"`.
///
/// **Example**
/// ```dart
/// foo(); // prints: Hello world!
/// ```
void foo() {
// My comment
print('foo!');
}
66 changes: 66 additions & 0 deletions dartdoc_test/lib/dartdoc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,69 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:io';

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:dartdoc_test/src/resource.dart';

import 'src/extractor.dart';

class DartDocTest {
const DartDocTest();

Future<void> run() async {
final rootFolder = Directory.current.absolute.path;
final contextCollection = AnalysisContextCollection(
includedPaths: [rootFolder],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);

final ctx = contextCollection.contextFor(rootFolder);
final session = ctx.currentSession;

// TODO: add `include` and `exclude` options
final files = Directory(rootFolder)
.listSync(recursive: true)
.whereType<File>()
.where((file) => file.path.endsWith('.dart'))
.toList();
for (final file in files) {
final result = session.getParsedUnit(file.path);
if (result is ParsedUnitResult) {
const extractor = Extractor();
final comments = extractor.extractDocumentationComments(result);
for (final c in comments) {
final samples = extractor.extractCodeSamples(c);
for (final s in samples) {
print(s.comment.span.start.toolString);
print(s.code);
}
writeCodeSamples(file.path, samples);
}
}
}
}

Future<void> runAnalyze() async {}
}

String wrapCode(String path, String code) {
return '''
import "$path";
void main() {
$code
}
''';
}

void writeCodeSamples(String filePath, List<DocumentationCodeSample> samples) {
for (final (i, s) in samples.indexed) {
final path = filePath.replaceAll('.dart', '_sample_$i.dart');
final code = wrapCode(filePath, s.code);
resourceProvider.setOverlay(path, content: code, modificationStamp: 0);
}
}
Loading

0 comments on commit 5ca5135

Please sign in to comment.