Skip to content

Commit 8411522

Browse files
authored
[tools] Validate pubspec topic format (#5565)
Fixes flutter/flutter#139305
1 parent 4147244 commit 8411522

File tree

2 files changed

+282
-1
lines changed

2 files changed

+282
-1
lines changed

script/tool/lib/src/pubspec_check_command.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ class PubspecCheckCommand extends PackageLoopingCommand {
332332
false;
333333
}
334334

335-
// Validates the "implements" keyword for a plugin, returning an error
335+
// Validates the "topics" keyword for a plugin, returning an error
336336
// string if there are any issues.
337337
String? _checkTopics(
338338
Pubspec pubspec, {
@@ -343,6 +343,10 @@ class PubspecCheckCommand extends PackageLoopingCommand {
343343
return 'A published package should include "topics". '
344344
'See https://dart.dev/tools/pub/pubspec#topics.';
345345
}
346+
if (topics.length > 5) {
347+
return 'A published package should have maximum 5 topics. '
348+
'See https://dart.dev/tools/pub/pubspec#topics.';
349+
}
346350
if (isFlutterPlugin(package) && package.isFederated) {
347351
final String pluginName = package.directory.parent.basename;
348352
// '_' isn't allowed in topics, so convert to '-'.
@@ -352,6 +356,19 @@ class PubspecCheckCommand extends PackageLoopingCommand {
352356
'a topic. Add "$topicName" to the "topics" section.';
353357
}
354358
}
359+
360+
// Validates topic names according to https://dart.dev/tools/pub/pubspec#topics
361+
final RegExp expectedTopicFormat = RegExp(r'^[a-z](?:-?[a-z0-9]+)*$');
362+
final Iterable<String> invalidTopics = topics.where((String topic) =>
363+
!expectedTopicFormat.hasMatch(topic) ||
364+
topic.length < 2 ||
365+
topic.length > 32);
366+
if (invalidTopics.isNotEmpty) {
367+
return 'Invalid topic(s): ${invalidTopics.join(', ')} in "topics" section. '
368+
'Topics must consist of lowercase alphanumerical characters or dash (but no double dash), '
369+
'start with a-z and ending with a-z or 0-9, have a minimum of 2 characters '
370+
'and have a maximum of 32 characters.';
371+
}
355372
return null;
356373
}
357374

script/tool/test/pubspec_check_command_test.dart

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,270 @@ ${_topicsSection()}
633633
);
634634
});
635635

636+
test('fails when topic name contains a space', () async {
637+
final RepositoryPackage plugin =
638+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
639+
640+
plugin.pubspecFile.writeAsStringSync('''
641+
${_headerSection('plugin')}
642+
${_environmentSection()}
643+
${_flutterSection(isPlugin: true)}
644+
${_dependenciesSection()}
645+
${_devDependenciesSection()}
646+
${_topicsSection(<String>['plugin a'])}
647+
''');
648+
649+
Error? commandError;
650+
final List<String> output = await runCapturingPrint(
651+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
652+
commandError = e;
653+
});
654+
655+
expect(commandError, isA<ToolExit>());
656+
expect(
657+
output,
658+
containsAllInOrder(<Matcher>[
659+
contains('Invalid topic(s): plugin a in "topics" section. '),
660+
]),
661+
);
662+
});
663+
664+
test('fails when topic a topic name contains double dash', () async {
665+
final RepositoryPackage plugin =
666+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
667+
668+
plugin.pubspecFile.writeAsStringSync('''
669+
${_headerSection('plugin')}
670+
${_environmentSection()}
671+
${_flutterSection(isPlugin: true)}
672+
${_dependenciesSection()}
673+
${_devDependenciesSection()}
674+
${_topicsSection(<String>['plugin--a'])}
675+
''');
676+
677+
Error? commandError;
678+
final List<String> output = await runCapturingPrint(
679+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
680+
commandError = e;
681+
});
682+
683+
expect(commandError, isA<ToolExit>());
684+
expect(
685+
output,
686+
containsAllInOrder(<Matcher>[
687+
contains('Invalid topic(s): plugin--a in "topics" section. '),
688+
]),
689+
);
690+
});
691+
692+
test('fails when topic a topic name starts with a number', () async {
693+
final RepositoryPackage plugin =
694+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
695+
696+
plugin.pubspecFile.writeAsStringSync('''
697+
${_headerSection('plugin')}
698+
${_environmentSection()}
699+
${_flutterSection(isPlugin: true)}
700+
${_dependenciesSection()}
701+
${_devDependenciesSection()}
702+
${_topicsSection(<String>['1plugin-a'])}
703+
''');
704+
705+
Error? commandError;
706+
final List<String> output = await runCapturingPrint(
707+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
708+
commandError = e;
709+
});
710+
711+
expect(commandError, isA<ToolExit>());
712+
expect(
713+
output,
714+
containsAllInOrder(<Matcher>[
715+
contains('Invalid topic(s): 1plugin-a in "topics" section. '),
716+
]),
717+
);
718+
});
719+
720+
test('fails when topic a topic name contains uppercase', () async {
721+
final RepositoryPackage plugin =
722+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
723+
724+
plugin.pubspecFile.writeAsStringSync('''
725+
${_headerSection('plugin')}
726+
${_environmentSection()}
727+
${_flutterSection(isPlugin: true)}
728+
${_dependenciesSection()}
729+
${_devDependenciesSection()}
730+
${_topicsSection(<String>['plugin-A'])}
731+
''');
732+
733+
Error? commandError;
734+
final List<String> output = await runCapturingPrint(
735+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
736+
commandError = e;
737+
});
738+
739+
expect(commandError, isA<ToolExit>());
740+
expect(
741+
output,
742+
containsAllInOrder(<Matcher>[
743+
contains('Invalid topic(s): plugin-A in "topics" section. '),
744+
]),
745+
);
746+
});
747+
748+
test('fails when there are more than 5 topics', () async {
749+
final RepositoryPackage plugin =
750+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
751+
752+
plugin.pubspecFile.writeAsStringSync('''
753+
${_headerSection('plugin')}
754+
${_environmentSection()}
755+
${_flutterSection(isPlugin: true)}
756+
${_dependenciesSection()}
757+
${_devDependenciesSection()}
758+
${_topicsSection(<String>[
759+
'plugin-a',
760+
'plugin-a',
761+
'plugin-a',
762+
'plugin-a',
763+
'plugin-a',
764+
'plugin-a'
765+
])}
766+
''');
767+
768+
Error? commandError;
769+
final List<String> output = await runCapturingPrint(
770+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
771+
commandError = e;
772+
});
773+
774+
expect(commandError, isA<ToolExit>());
775+
expect(
776+
output,
777+
containsAllInOrder(<Matcher>[
778+
contains(
779+
' A published package should have maximum 5 topics. See https://dart.dev/tools/pub/pubspec#topics.'),
780+
]),
781+
);
782+
});
783+
784+
test('fails if a topic name is longer than 32 characters', () async {
785+
final RepositoryPackage plugin =
786+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
787+
788+
plugin.pubspecFile.writeAsStringSync('''
789+
${_headerSection('plugin')}
790+
${_environmentSection()}
791+
${_flutterSection(isPlugin: true)}
792+
${_dependenciesSection()}
793+
${_devDependenciesSection()}
794+
${_topicsSection(<String>['foobarfoobarfoobarfoobarfoobarfoobarfoo'])}
795+
''');
796+
797+
Error? commandError;
798+
final List<String> output = await runCapturingPrint(
799+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
800+
commandError = e;
801+
});
802+
803+
expect(commandError, isA<ToolExit>());
804+
expect(
805+
output,
806+
containsAllInOrder(<Matcher>[
807+
contains(
808+
'Invalid topic(s): foobarfoobarfoobarfoobarfoobarfoobarfoo in "topics" section. '),
809+
]),
810+
);
811+
});
812+
813+
test('fails if a topic name is longer than 2 characters', () async {
814+
final RepositoryPackage plugin =
815+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
816+
817+
plugin.pubspecFile.writeAsStringSync('''
818+
${_headerSection('plugin')}
819+
${_environmentSection()}
820+
${_flutterSection(isPlugin: true)}
821+
${_dependenciesSection()}
822+
${_devDependenciesSection()}
823+
${_topicsSection(<String>['a'])}
824+
''');
825+
826+
Error? commandError;
827+
final List<String> output = await runCapturingPrint(
828+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
829+
commandError = e;
830+
});
831+
832+
expect(commandError, isA<ToolExit>());
833+
expect(
834+
output,
835+
containsAllInOrder(<Matcher>[
836+
contains('Invalid topic(s): a in "topics" section. '),
837+
]),
838+
);
839+
});
840+
841+
test('fails if a topic name ends in a dash', () async {
842+
final RepositoryPackage plugin =
843+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
844+
845+
plugin.pubspecFile.writeAsStringSync('''
846+
${_headerSection('plugin')}
847+
${_environmentSection()}
848+
${_flutterSection(isPlugin: true)}
849+
${_dependenciesSection()}
850+
${_devDependenciesSection()}
851+
${_topicsSection(<String>['plugin-'])}
852+
''');
853+
854+
Error? commandError;
855+
final List<String> output = await runCapturingPrint(
856+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
857+
commandError = e;
858+
});
859+
860+
expect(commandError, isA<ToolExit>());
861+
expect(
862+
output,
863+
containsAllInOrder(<Matcher>[
864+
contains('Invalid topic(s): plugin- in "topics" section. '),
865+
]),
866+
);
867+
});
868+
869+
test('Invalid topics section has expected error message', () async {
870+
final RepositoryPackage plugin =
871+
createFakePlugin('plugin', packagesDir, examples: <String>[]);
872+
873+
plugin.pubspecFile.writeAsStringSync('''
874+
${_headerSection('plugin')}
875+
${_environmentSection()}
876+
${_flutterSection(isPlugin: true)}
877+
${_dependenciesSection()}
878+
${_devDependenciesSection()}
879+
${_topicsSection(<String>['plugin-A', 'Plugin-b'])}
880+
''');
881+
882+
Error? commandError;
883+
final List<String> output = await runCapturingPrint(
884+
runner, <String>['pubspec-check'], errorHandler: (Error e) {
885+
commandError = e;
886+
});
887+
888+
expect(commandError, isA<ToolExit>());
889+
expect(
890+
output,
891+
containsAllInOrder(<Matcher>[
892+
contains('Invalid topic(s): plugin-A, Plugin-b in "topics" section. '
893+
'Topics must consist of lowercase alphanumerical characters or dash (but no double dash), '
894+
'start with a-z and ending with a-z or 0-9, have a minimum of 2 characters '
895+
'and have a maximum of 32 characters.'),
896+
]),
897+
);
898+
});
899+
636900
test('fails when environment section is out of order', () async {
637901
final RepositoryPackage plugin =
638902
createFakePlugin('plugin', packagesDir, examples: <String>[]);

0 commit comments

Comments
 (0)