Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion script/tool/lib/src/pubspec_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ class PubspecCheckCommand extends PackageLoopingCommand {
false;
}

// Validates the "implements" keyword for a plugin, returning an error
// Validates the "topics" keyword for a plugin, returning an error
// string if there are any issues.
String? _checkTopics(
Pubspec pubspec, {
Expand All @@ -343,6 +343,10 @@ class PubspecCheckCommand extends PackageLoopingCommand {
return 'A published package should include "topics". '
'See https://dart.dev/tools/pub/pubspec#topics.';
}
if (topics.length > 5) {
return 'A published package should have maximum 5 topics. '
'See https://dart.dev/tools/pub/pubspec#topics.';
}
if (isFlutterPlugin(package) && package.isFederated) {
final String pluginName = package.directory.parent.basename;
// '_' isn't allowed in topics, so convert to '-'.
Expand All @@ -352,6 +356,19 @@ class PubspecCheckCommand extends PackageLoopingCommand {
'a topic. Add "$topicName" to the "topics" section.';
}
}

// Validates topic names according to https://dart.dev/tools/pub/pubspec#topics
final RegExp expectedTopicFormat = RegExp(r'^[a-z](?:-?[a-z0-9]+)*$');
final Iterable<String> invalidTopics = topics.where((String topic) =>
!expectedTopicFormat.hasMatch(topic) ||
topic.length < 2 ||
topic.length > 32);
if (invalidTopics.isNotEmpty) {
return 'Invalid topic(s): ${invalidTopics.join(', ')} in "topics" section. '
'Topics must consist of lowercase alphanumerical characters or dash (but no double dash), '
'start with a-z and ending with a-z or 0-9, have a minimum of 2 characters '
'and have a maximum of 32 characters.';
}
return null;
}

Expand Down
264 changes: 264 additions & 0 deletions script/tool/test/pubspec_check_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,270 @@ ${_topicsSection()}
);
});

test('fails when topic name contains a space', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin a'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): plugin a in "topics" section. '),
]),
);
});

test('fails when topic a topic name contains double dash', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin--a'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): plugin--a in "topics" section. '),
]),
);
});

test('fails when topic a topic name starts with a number', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['1plugin-a'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): 1plugin-a in "topics" section. '),
]),
);
});

test('fails when topic a topic name contains uppercase', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-A'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): plugin-A in "topics" section. '),
]),
);
});

test('fails when there are more than 5 topics', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>[
'plugin-a',
'plugin-a',
'plugin-a',
'plugin-a',
'plugin-a',
'plugin-a'
])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
' A published package should have maximum 5 topics. See https://dart.dev/tools/pub/pubspec#topics.'),
]),
);
});

test('fails if a topic name is longer than 32 characters', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['foobarfoobarfoobarfoobarfoobarfoobarfoo'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Invalid topic(s): foobarfoobarfoobarfoobarfoobarfoobarfoo in "topics" section. '),
]),
);
});

test('fails if a topic name is longer than 2 characters', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['a'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): a in "topics" section. '),
]),
);
});

test('fails if a topic name ends in a dash', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): plugin- in "topics" section. '),
]),
);
});

test('Invalid topics section has expected error message', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);

plugin.pubspecFile.writeAsStringSync('''
${_headerSection('plugin')}
${_environmentSection()}
${_flutterSection(isPlugin: true)}
${_dependenciesSection()}
${_devDependenciesSection()}
${_topicsSection(<String>['plugin-A', 'Plugin-b'])}
''');

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['pubspec-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Invalid topic(s): plugin-A, Plugin-b in "topics" section. '
'Topics must consist of lowercase alphanumerical characters or dash (but no double dash), '
'start with a-z and ending with a-z or 0-9, have a minimum of 2 characters '
'and have a maximum of 32 characters.'),
]),
);
});

test('fails when environment section is out of order', () async {
final RepositoryPackage plugin =
createFakePlugin('plugin', packagesDir, examples: <String>[]);
Expand Down