Skip to content

Commit f5a9835

Browse files
authored
Check sample links for malformed links (flutter#137807)
## Description This checks API doc strings for malformed links to examples. It prevents errors in capitalization, spacing, number of asterisks, etc. It won't catch all errors, because it needs to have a minimally indicative string to know that it even is trying to be a link to an example. At a minimum, the line needs to look like (literally, not as a regexp) `///*seecode.*` in order to be seen as a link to an example. Separately, I'm going to add a check to the snippets tool that checks to make sure that an `{@tool}` block includes either a link to a sample file or a dart code block. ## Tests - Added a test to make sure it catches some malformed links.
1 parent defa4bc commit f5a9835

File tree

2 files changed

+105
-20
lines changed

2 files changed

+105
-20
lines changed

dev/bots/check_code_samples.dart

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ void main(List<String> args) {
9797
reportSuccessAndExit('All examples are linked and have tests.');
9898
}
9999

100+
class LinkInfo {
101+
const LinkInfo(this.link, this.file, this.line);
102+
103+
final String link;
104+
final File file;
105+
final int line;
106+
107+
@override
108+
String toString() {
109+
return '${file.path}:$line: $link';
110+
}
111+
}
112+
100113
class SampleChecker {
101114
SampleChecker({
102115
required this.examples,
@@ -119,10 +132,12 @@ class SampleChecker {
119132
final List<File> exampleFilenames = getExampleFilenames(examples);
120133

121134
// Get a list of all the example link paths that appear in the source files.
122-
final Set<String> exampleLinks = getExampleLinks(packages);
123-
135+
final (Set<String> exampleLinks, Set<LinkInfo> malformedLinks) = getExampleLinks(packages);
124136
// Also add in any that might be found in the dart:ui directory.
125-
exampleLinks.addAll(getExampleLinks(dartUIPath));
137+
final (Set<String> uiExampleLinks, Set<LinkInfo> uiMalformedLinks) = getExampleLinks(dartUIPath);
138+
139+
exampleLinks.addAll(uiExampleLinks);
140+
malformedLinks.addAll(uiMalformedLinks);
126141

127142
// Get a list of the filenames that were not found in the source files.
128143
final List<String> missingFilenames = checkForMissingLinks(exampleFilenames, exampleLinks);
@@ -136,7 +151,7 @@ class SampleChecker {
136151
// generate new examples.
137152
missingFilenames.removeWhere((String file) => _knownUnlinkedExamples.contains(file));
138153

139-
if (missingFilenames.isEmpty && missingTests.isEmpty && noLongerMissing.isEmpty) {
154+
if (missingFilenames.isEmpty && missingTests.isEmpty && noLongerMissing.isEmpty && malformedLinks.isEmpty) {
140155
return true;
141156
}
142157

@@ -167,6 +182,19 @@ class SampleChecker {
167182
buffer.write('Either link them to a source file API doc comment, or remove them.');
168183
foundError(buffer.toString().split('\n'));
169184
}
185+
186+
if (malformedLinks.isNotEmpty) {
187+
final StringBuffer buffer =
188+
StringBuffer('The following malformed links were found in API doc comments:\n');
189+
for (final LinkInfo link in malformedLinks) {
190+
buffer.writeln(' $link');
191+
}
192+
buffer.write(
193+
'Correct the formatting of these links so that they match the exact pattern:\n'
194+
r" r'\*\* See code in (?<path>.+) \*\*'"
195+
);
196+
foundError(buffer.toString().split('\n'));
197+
}
170198
return false;
171199
}
172200

@@ -199,21 +227,34 @@ class SampleChecker {
199227
);
200228
}
201229

202-
Set<String> getExampleLinks(Directory searchDirectory) {
230+
(Set<String>, Set<LinkInfo>) getExampleLinks(Directory searchDirectory) {
203231
final List<File> files = getFiles(searchDirectory, RegExp(r'\.dart$'));
204232
final Set<String> searchStrings = <String>{};
205-
final RegExp exampleRe = RegExp(r'\*\* See code in (?<path>.*) \*\*');
233+
final Set<LinkInfo> malformedStrings = <LinkInfo>{};
234+
final RegExp validExampleRe = RegExp(r'\*\* See code in (?<path>.+) \*\*');
235+
// Looks for some common broken versions of example links. This looks for
236+
// something that is at minimum "///*seecode<something>*" to indicate that it
237+
// looks like an example link. It should be narrowed if we start gettting false
238+
// positives.
239+
final RegExp malformedLinkRe = RegExp(r'^(?<malformed>\s*///\s*\*\*?\s*[sS][eE][eE]\s*[Cc][Oo][Dd][Ee].+\*\*?)');
206240
for (final File file in files) {
207241
final String contents = file.readAsStringSync();
208-
searchStrings.addAll(
209-
contents.split('\n').where((String s) => s.contains(exampleRe)).map<String>(
210-
(String e) {
211-
return exampleRe.firstMatch(e)!.namedGroup('path')!;
212-
},
213-
),
214-
);
242+
final List<String> lines = contents.split('\n');
243+
int count = 0;
244+
for (final String line in lines) {
245+
count += 1;
246+
final RegExpMatch? validMatch = validExampleRe.firstMatch(line);
247+
if (validMatch != null) {
248+
searchStrings.add(validMatch.namedGroup('path')!);
249+
}
250+
final RegExpMatch? malformedMatch = malformedLinkRe.firstMatch(line);
251+
// It's only malformed if it doesn't match the valid RegExp.
252+
if (malformedMatch != null && validMatch == null) {
253+
malformedStrings.add(LinkInfo(malformedMatch.namedGroup('malformed')!, file, count));
254+
}
255+
}
215256
}
216-
return searchStrings;
257+
return (searchStrings, malformedStrings);
217258
}
218259

219260
List<String> checkForMissingLinks(List<File> exampleFilenames, Set<String> searchStrings) {

dev/bots/test/check_code_samples_test.dart

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ void main() {
2525
return path.relative(file.absolute.path, from: flutterRoot.absolute.path);
2626
}
2727

28-
void writeLink({required File source, required File example}) {
28+
void writeLink({required File source, required File example, String? alternateLink}) {
29+
final String link = alternateLink ?? ' ** See code in ${getRelativePath(example)} **';
2930
source
3031
..createSync(recursive: true)
3132
..writeAsStringSync('''
@@ -34,12 +35,12 @@ void main() {
3435
/// {@tool dartpad}
3536
/// Example description
3637
///
37-
/// ** See code in ${getRelativePath(example)} **
38+
///$link
3839
/// {@end-tool}
3940
''');
4041
}
4142

42-
void buildTestFiles({bool missingLinks = false, bool missingTests = false}) {
43+
void buildTestFiles({bool missingLinks = false, bool missingTests = false, bool malformedLinks = false}) {
4344
final Directory examplesLib = examples.childDirectory('lib').childDirectory('layer')..createSync(recursive: true);
4445
final File fooExample = examplesLib.childFile('foo_example.0.dart')
4546
..createSync(recursive: true)
@@ -73,9 +74,15 @@ void main() {
7374
}
7475
final Directory flutterPackage = packages.childDirectory('flutter').childDirectory('lib').childDirectory('src')
7576
..createSync(recursive: true);
76-
writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample);
77-
writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample);
78-
writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample);
77+
if (malformedLinks) {
78+
writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample, alternateLink: '*See Code *');
79+
writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample, alternateLink: ' ** See code examples/api/lib/layer/bar_example.0.dart **');
80+
writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample, alternateLink: '* see code in examples/api/lib/animation/curves/curve2_d.0.dart *');
81+
} else {
82+
writeLink(source: flutterPackage.childDirectory('layer').childFile('foo.dart'), example: fooExample);
83+
writeLink(source: flutterPackage.childDirectory('layer').childFile('bar.dart'), example: barExample);
84+
writeLink(source: flutterPackage.childDirectory('animation').childFile('curves.dart'), example: curvesExample);
85+
}
7986
}
8087

8188
setUp(() {
@@ -124,6 +131,43 @@ void main() {
124131
expect(success, equals(false));
125132
});
126133

134+
test('check_code_samples.dart - checkCodeSamples catches malformed links', () async {
135+
buildTestFiles(malformedLinks: true);
136+
bool? success;
137+
final String result = await capture(
138+
() async {
139+
success = checker.checkCodeSamples();
140+
},
141+
shouldHaveErrors: true,
142+
);
143+
final bool isWindows = Platform.isWindows;
144+
final String lines = <String>[
145+
'╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════',
146+
'║ The following examples are not linked from any source file API doc comments:',
147+
if (!isWindows) '║ examples/api/lib/animation/curves/curve2_d.0.dart',
148+
if (!isWindows) '║ examples/api/lib/layer/foo_example.0.dart',
149+
if (!isWindows) '║ examples/api/lib/layer/bar_example.0.dart',
150+
if (isWindows) r'║ examples\api\lib\animation\curves\curve2_d.0.dart',
151+
if (isWindows) r'║ examples\api\lib\layer\foo_example.0.dart',
152+
if (isWindows) r'║ examples\api\lib\layer\bar_example.0.dart',
153+
'║ Either link them to a source file API doc comment, or remove them.',
154+
'╚═══════════════════════════════════════════════════════════════════════════════',
155+
'╔═╡ERROR╞═══════════════════════════════════════════════════════════════════════',
156+
'║ The following malformed links were found in API doc comments:',
157+
if (!isWindows) '║ /flutter sdk/packages/flutter/lib/src/animation/curves.dart:6: ///* see code in examples/api/lib/animation/curves/curve2_d.0.dart *',
158+
if (!isWindows) '║ /flutter sdk/packages/flutter/lib/src/layer/foo.dart:6: ///*See Code *',
159+
if (!isWindows) '║ /flutter sdk/packages/flutter/lib/src/layer/bar.dart:6: /// ** See code examples/api/lib/layer/bar_example.0.dart **',
160+
if (isWindows) r'║ C:\flutter sdk\packages\flutter\lib\src\animation\curves.dart:6: ///* see code in examples/api/lib/animation/curves/curve2_d.0.dart *',
161+
if (isWindows) r'║ C:\flutter sdk\packages\flutter\lib\src\layer\foo.dart:6: ///*See Code *',
162+
if (isWindows) r'║ C:\flutter sdk\packages\flutter\lib\src\layer\bar.dart:6: /// ** See code examples/api/lib/layer/bar_example.0.dart **',
163+
'║ Correct the formatting of these links so that they match the exact pattern:',
164+
r"║ r'\*\* See code in (?<path>.+) \*\*'",
165+
'╚═══════════════════════════════════════════════════════════════════════════════',
166+
].join('\n');
167+
expect(result, equals('$lines\n'));
168+
expect(success, equals(false));
169+
});
170+
127171
test('check_code_samples.dart - checkCodeSamples catches missing tests', () async {
128172
buildTestFiles(missingTests: true);
129173
bool? success;

0 commit comments

Comments
 (0)