Skip to content

Commit

Permalink
Run and process dartdoc output to create pub-data.json and the covera…
Browse files Browse the repository at this point in the history
…ge report. (#1270)

* Run and process dartdoc to create pub-data.json and the coverage report.

* Do not retry dartdoc.

* Refactored package_context.dart code.
  • Loading branch information
isoos authored Nov 3, 2023
1 parent aa681c8 commit 963ff83
Show file tree
Hide file tree
Showing 35 changed files with 825 additions and 185 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.21.41

- (Optionally enabled) `dartdoc`-based report (the same that pub-dev is using).
- **Breaking change**: when the dartdoc output directory is specified (and also
when running `bin/pana`), running the global dartdoc has become the default (instead
of the SDK's `dart doc`), because the latest SDK is behind the latest `dartdoc`
version, and we want to control what is being used by the analysis.
- **Breaking change**: `ToolEnvironment.dartdoc` returns `PanaProcessResult`.

## 0.21.40

- Fix: empty `yaml` parsing.
Expand Down
12 changes: 9 additions & 3 deletions bin/pana.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,21 @@ Future main(List<String> args) async {
exit(130);
});

var tempDir = Directory.systemTemp
final tempDir = Directory.systemTemp
.createTempSync('pana.${DateTime.now().millisecondsSinceEpoch}.');

// Critical to make sure analyzer paths align well
var tempPath = await tempDir.resolveSymbolicLinks();
final tempPath = await tempDir.resolveSymbolicLinks();

final pubCacheDir = p.join(tempPath, 'pub-cache');
await Directory(pubCacheDir).create(recursive: true);
final dartdocOutputDir = p.join(tempPath, 'doc');
await Directory(dartdocOutputDir).create(recursive: true);

try {
final pubHostedUrl = result['hosted-url'] as String?;
final analyzer = await PackageAnalyzer.create(
pubCacheDir: tempPath,
pubCacheDir: pubCacheDir,
panaCacheDir: Platform.environment['PANA_CACHE'],
sdkDir: result['dart-sdk'] as String?,
flutterDir: result['flutter-sdk'] as String?,
Expand All @@ -146,6 +151,7 @@ Future main(List<String> args) async {
pubHostedUrl: pubHostedUrl,
lineLength: int.tryParse(result['line-length'] as String? ?? ''),
checkRemoteRepository: true,
dartdocOutputDir: dartdocOutputDir,
);
try {
late Summary summary;
Expand Down
79 changes: 79 additions & 0 deletions lib/src/dartdoc/dartdoc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2023, 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 'dart:io';

import 'package:path/path.dart' as p;

import '../model.dart';
import '../report/_common.dart';
import 'dartdoc_index.dart';
import 'index_to_pubdata.dart';
import 'pub_dartdoc_data.dart';

final dartdocSubsectionHeadline =
'20% or more of the public API has dartdoc comments';

Future<PubDartdocData> generateAndSavePubDataJson(
String dartdocOutputDir) async {
final content =
await File(p.join(dartdocOutputDir, 'index.json')).readAsString();
final index = DartdocIndex.parseJsonText(content);
final data = dataFromDartdocIndex(index);
await File(p.join(dartdocOutputDir, 'pub-data.json'))
.writeAsString(json.encode(data.toJson()));
return data;
}

Subsection dartdocFailedSubsection(String reason) {
return Subsection(
dartdocSubsectionHeadline,
[RawParagraph(reason)],
0,
10,
ReportStatus.failed,
);
}

Future<Subsection> createDocumentationCoverageSection(
PubDartdocData data) async {
final documented = data.coverage?.documented ?? 0;
final total = data.coverage?.total ?? 0;
final symbolsMissingDocumentation = data.apiElements
?.where((e) => e.documentation == null || e.documentation!.isEmpty)
.map((e) => e.name)
.toList();

final maxPoints = 10;
final ratio = total <= 0 ? 1.0 : documented / total;
final accepted = ratio >= 0.2;
final percent = (100.0 * ratio).toStringAsFixed(1);
final summary = StringBuffer();
final grantedPoints = accepted ? maxPoints : 0;
summary.write(
'$documented out of $total API elements ($percent %) have documentation comments.');

if (!accepted) {
summary.write('\n\n'
'Providing good documentation for libraries, classes, functions, and other API '
'elements improves code readability and helps developers find and use your API. '
'Document at least 20% of the public API elements.');
}

if (symbolsMissingDocumentation != null &&
symbolsMissingDocumentation.isNotEmpty) {
summary.write('\n\n'
'Some symbols that are missing documentation: '
'${symbolsMissingDocumentation.take(5).map((e) => '`$e`').join(', ')}.');
}

return Subsection(
dartdocSubsectionHeadline,
[RawParagraph(summary.toString())],
grantedPoints,
maxPoints,
grantedPoints == maxPoints ? ReportStatus.passed : ReportStatus.partial,
);
}
119 changes: 119 additions & 0 deletions lib/src/dartdoc/dartdoc_index.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2023, 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:json_annotation/json_annotation.dart';

part 'dartdoc_index.g.dart';

// TODO(https://github.com/dart-lang/pana/issues/1273): remove these and use a different pub-data.json file format.
const kindNames = <int, String>{
0: 'accessor',
1: 'constant',
2: 'constructor',
3: 'class',
4: 'dynamic',
5: 'enum',
6: 'extension',
7: 'extension type',
8: 'function',
9: 'library',
10: 'method',
11: 'mixin',
12: 'Never',
13: 'package',
14: 'parameter',
15: 'prefix',
16: 'property',
17: 'SDK',
18: 'topic',
19: 'top-level constant',
20: 'top-level property',
21: 'typedef',
22: 'type parameter',
};

/// The parsed content of the `index.json` generated by dartdoc.
class DartdocIndex {
final List<DartdocIndexEntry> entries;

DartdocIndex(this.entries);

factory DartdocIndex.parseJsonText(String content) {
return DartdocIndex.fromJsonList(json.decode(content) as List);
}

factory DartdocIndex.fromJsonList(List jsonList) {
final list = jsonList
.map((item) => DartdocIndexEntry.fromJson(item as Map<String, dynamic>))
.toList();
return DartdocIndex(list);
}

late final libraryRelativeUrls = Map<String, String>.fromEntries(
entries
.where((e) => e.isLibrary && e.qualifiedName != null && e.href != null)
.map(
(e) => MapEntry<String, String>(
e.qualifiedName!.split('.').first,
e.href!,
),
),
);

String toJsonText() => json.encode(entries);
}

@JsonSerializable(includeIfNull: false)
class DartdocIndexEntry {
final String? name;
final String? qualifiedName;
final String? href;
final int? kind;
final int? packageRank;
final int? overriddenDepth;
final String? packageName;
final String? desc;
final DartdocIndexEntryEnclosedBy? enclosedBy;

DartdocIndexEntry({
required this.name,
required this.qualifiedName,
required this.href,
this.kind,
this.packageRank,
this.overriddenDepth,
this.packageName,
this.desc,
this.enclosedBy,
});

factory DartdocIndexEntry.fromJson(Map<String, dynamic> json) =>
_$DartdocIndexEntryFromJson(json);

Map<String, dynamic> toJson() => _$DartdocIndexEntryToJson(this);

/// Wether the entry is a top-level library.
bool get isLibrary => href != null && href!.endsWith('-library.html');
bool get isClass => href != null && href!.endsWith('-class.html');
}

@JsonSerializable(includeIfNull: false)
class DartdocIndexEntryEnclosedBy {
final String? name;
final int? kind;
final String? href;

DartdocIndexEntryEnclosedBy({
this.name,
this.kind,
this.href,
});

factory DartdocIndexEntryEnclosedBy.fromJson(Map<String, dynamic> json) =>
_$DartdocIndexEntryEnclosedByFromJson(json);

Map<String, dynamic> toJson() => _$DartdocIndexEntryEnclosedByToJson(this);
}
70 changes: 70 additions & 0 deletions lib/src/dartdoc/dartdoc_index.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions lib/src/dartdoc/dartdoc_options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2023, 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 'dart:io';

import 'package:path/path.dart' as p;

import '../utils.dart' show yamlToJson;

Future<void> normalizeDartdocOptionsYaml(String packageDir) async {
final optionsFile = File(p.join(packageDir, 'dartdoc_options.yaml'));
Map<String, dynamic>? originalContent;
try {
originalContent = yamlToJson(await optionsFile.readAsString());
} on IOException {
// pass, ignore missing file
} on FormatException {
// pass, ignore broken file
}
final updatedContent = _customizeDartdocOptions(originalContent);
await optionsFile.writeAsString(json.encode(updatedContent));
}

/// Returns a new, pub-specific dartdoc options based on [original].
///
/// dartdoc_options.yaml allows to change how doc content is generated.
/// To provide uniform experience across the pub site, and to reduce the
/// potential attack surface (HTML-, and code-injections, code executions),
/// we do not support every option.
///
/// https://github.com/dart-lang/dartdoc#dartdoc_optionsyaml
///
/// Discussion on the enabled options:
/// https://github.com/dart-lang/pub-dev/issues/4521#issuecomment-779821098
Map<String, dynamic> _customizeDartdocOptions(Map<String, dynamic>? original) {
final passThroughOptions = <String, dynamic>{};
if (original != null &&
original.containsKey('dartdoc') &&
original['dartdoc'] is Map<String, dynamic>) {
final dartdoc = original['dartdoc'] as Map<String, dynamic>;
for (final key in _passThroughKeys) {
if (dartdoc.containsKey(key)) {
passThroughOptions[key] = dartdoc[key];
}
}
}
return <String, dynamic>{
'dartdoc': <String, dynamic>{
...passThroughOptions,
'showUndocumentedCategories': true,
},
};
}

final _passThroughKeys = <String>[
'categories',
'categoryOrder',
// Note: consider enabling after checking that the relative path doesn't escape the package folder
// 'examplePathPrefix',
'exclude',
'include',
'nodoc',
];
Loading

0 comments on commit 963ff83

Please sign in to comment.