diff --git a/dartdoc_test/bin/analyze.dart b/dartdoc_test/bin/analyze.dart new file mode 100644 index 00000000..7080e7d0 --- /dev/null +++ b/dartdoc_test/bin/analyze.dart @@ -0,0 +1,6 @@ +import 'package:dartdoc_test/dartdoc_test.dart'; + +Future main(List args) async { + final dartdocTest = DartDocTest(); + dartdocTest.runAnalyze(); +} diff --git a/dartdoc_test/bin/dartdoc_test.dart b/dartdoc_test/bin/dartdoc_test.dart index 20a5bbe7..d4ddb9cf 100644 --- a/dartdoc_test/bin/dartdoc_test.dart +++ b/dartdoc_test/bin/dartdoc_test.dart @@ -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 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 extractDocumentationComments(ParsedUnitResult r) { - final file = SourceFile.fromString(r.content, url: r.uri); - final comments = []; - 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 { - 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 extractCodeSamples(DocumentationComment comment) { - final samples = []; - 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 { - 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 main(List args) async { + final dartdocTest = DartDocTest(); + dartdocTest.run(); } diff --git a/dartdoc_test/example/example.dart b/dartdoc_test/example/example.dart new file mode 100644 index 00000000..683b3f00 --- /dev/null +++ b/dartdoc_test/example/example.dart @@ -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!'); +} diff --git a/dartdoc_test/example/main.dart b/dartdoc_test/example/main.dart index 8ee2268a..22831e46 100644 --- a/dartdoc_test/example/main.dart +++ b/dartdoc_test/example/main.dart @@ -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!'); +} diff --git a/dartdoc_test/lib/dartdoc_test.dart b/dartdoc_test/lib/dartdoc_test.dart index 0c58130a..519ec5e0 100644 --- a/dartdoc_test/lib/dartdoc_test.dart +++ b/dartdoc_test/lib/dartdoc_test.dart @@ -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 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() + .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 runAnalyze() async {} +} + +String wrapCode(String path, String code) { + return ''' +import "$path"; + +void main() { + $code +} +'''; +} + +void writeCodeSamples(String filePath, List 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); + } +} diff --git a/dartdoc_test/lib/src/extractor.dart b/dartdoc_test/lib/src/extractor.dart new file mode 100644 index 00000000..4f49a153 --- /dev/null +++ b/dartdoc_test/lib/src/extractor.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:markdown/markdown.dart'; +import 'package:source_span/source_span.dart'; + +final _md = Document(extensionSet: ExtensionSet.gitHubWeb); + +class DocumentationComment { + final FileSpan span; + final String contents; + + DocumentationComment({ + required this.contents, + required this.span, + }); +} + +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, + }); +} + +class Extractor { + const Extractor(); + + void extractFile() {} + + List extractDocumentationComments(ParsedUnitResult r) { + final file = SourceFile.fromString(r.content, url: r.uri); + final comments = []; + r.unit.accept(_ForEachCommentAstVisitor((comment) { + if (comment.isDocumentation) { + final span = file.span(comment.offset, comment.end); + + // TODO: remove `///` syntax with a better way.. + var lines = LineSplitter.split(span.text); + lines = lines.map((l) => l.replaceFirst('///', '')); + comments.add(DocumentationComment( + contents: lines.join('\n'), + span: span, + )); + } + })); + return comments; + } + + List extractCodeSamples( + DocumentationComment comment, + ) { + final samples = []; + 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; + } +} + +class _ForEachCommentAstVisitor extends RecursiveAstVisitor { + final void Function(Comment comment) _forEach; + + _ForEachCommentAstVisitor(this._forEach); + + @override + void visitComment(Comment node) => _forEach(node); +} + +extension on List { + 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); +} diff --git a/dartdoc_test/lib/src/resource.dart b/dartdoc_test/lib/src/resource.dart new file mode 100644 index 00000000..02a226df --- /dev/null +++ b/dartdoc_test/lib/src/resource.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/file_system/overlay_file_system.dart'; + +final resourceProvider = + OverlayResourceProvider(PhysicalResourceProvider.INSTANCE); + +final _current = Directory.current; + +void currentContext = AnalysisContextCollection( + resourceProvider: resourceProvider, + includedPaths: [_current.absolute.path], +).contextFor(_current.absolute.path); diff --git a/dartdoc_test/pubspec.yaml b/dartdoc_test/pubspec.yaml index ec096110..26607c1a 100644 --- a/dartdoc_test/pubspec.yaml +++ b/dartdoc_test/pubspec.yaml @@ -10,9 +10,10 @@ dev_dependencies: test: ^1.24.0 lints: ^2.0.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: "3.3.0" dependencies: analyzer: ^5.10.0 + args: ^2.5.0 markdown: ^7.0.2 path: ^1.8.3 source_span: ^1.10.0