diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index bccdad5c78..378dfc65ca 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -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, { @@ -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 '-'. @@ -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 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; } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 60bcc2d478..8e99d720f5 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -633,6 +633,270 @@ ${_topicsSection()} ); }); + test('fails when topic name contains a space', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['plugin a'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['plugin--a'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['1plugin-a'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['plugin-A'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection([ + 'plugin-a', + 'plugin-a', + 'plugin-a', + 'plugin-a', + 'plugin-a', + 'plugin-a' + ])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['foobarfoobarfoobarfoobarfoobarfoobarfoo'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['a'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['plugin-'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Invalid topic(s): plugin- in "topics" section. '), + ]), + ); + }); + + test('Invalid topics section has expected error message', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_topicsSection(['plugin-A', 'Plugin-b'])} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: []);