[tools] Validate pubspec topic format (#5565)

Fixes https://github.com/flutter/flutter/issues/139305
This commit is contained in:
LouiseHsu
2023-12-12 08:44:27 -08:00
committed by GitHub
parent 4147244c63
commit 8411522be7
2 changed files with 282 additions and 1 deletions

View File

@ -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<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;
}

View File

@ -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>[]);