refactor: update automatic documentation generation#315
Conversation
…o 101-docs-structured-parameters
…d update parameter metadata
…rove rule parsing logic
There was a problem hiding this comment.
Code Review
This pull request refactors the documentation parser for solid_lints by introducing a new DocsParser orchestrator, ParserRegexes, and a revamped ParserUtils utility class. It adds support for @docType custom types, {@template} scanning, and {@macro} expansion, while updating configuration formats to use the new plugins: solid_lints structure. Feedback on the changes highlights two issues: a potential null-pointer evaluation to 'null' when parsing fields without explicit types in ParametersParser, and a logic bug in the circular macro dependency check within ParserUtils.expandMacros that could trigger false positives on valid nested macro expansions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…rcular dependency detection logic
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the configuration structure of solid_lints to use the new plugins block, updates Docusaurus dependencies, and introduces @doctype annotations across several parameter classes. Additionally, it refactors the documentation parser utility to support custom type scanning, template/macro expansion, and MDX-safe escaping, while making lintName public across lint rules. The review feedback suggests improving error handling during documentation generation by including the field name in missing-documentation errors and passing the file path context to macro expansion errors to aid debugging.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…ontext and specific field names
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the solid_lints configuration format to use the new plugins syntax, updates documentation, and significantly refactors the documentation parser to support custom types, templates, and macro expansion. Feedback on the changes includes two recommendations: first, to use uri.pathSegments.last instead of p.basenameWithoutExtension in ParserUtils.fileNameSuffix to prevent platform-dependent path parsing issues on Windows; second, to use toSource() instead of toString() on AST TypeAnnotation nodes in ParametersParser to ensure robustness across different versions of the analyzer package.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
/gemini review |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
|
/gemini review |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
|
/gemini review |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
There was a problem hiding this comment.
I kinda feel like it all can be put into a single method with some helper extensions extracted:
T parse(Directory dir, {bool sortRulesAlphabetically = true}) {
final files = dir
.listSync(recursive: true)
.whereType<File>()
.where(
(f) => ruleFileSuffixes.contains(ParserUtils.fileNameSuffix(f.uri)),
)
.map((file) => file.path)
.toList();
if (files.isEmpty) throw 'Found no rules in specified directory';
final libDir = ParserUtils.findLibDir(dir);
final customTypes = {
'DiagnosticSeverity': 'String',
for (final entity in libDir.listSync(recursive: true))
if (entity case File(hasDocStrings: true, :final declarations?))
for (final declaration in declarations)
if (declaration case CompilationUnitMember(
:final name?,
:final type?,
))
name: type.trim(),
};
final templates = ParserUtils.scanForTemplates(libDir);
final docs = [
for (final path in files)
RuleParser(
rulePath: path,
customTypes: customTypes,
templates: templates,
).parse(),
];
log('Parsed ${docs.length} rules');
return formatter.format(
sortRulesAlphabetically ? docs.sortedBy((e) => e.name) : docs.toList(),
);
}
}and somewhere in utils:
extension on File {
bool get hasDocStrings =>
path.endsWith('.dart') && readAsStringSync().contains('@docType');
NodeList<CompilationUnitMember>? get declarations =>
tryOrNull(() => ParserUtils.parseAst(path).declarations);
}extension on CompilationUnitMember {
String? get name =>
tryOrNull<String?>(() => ParserUtils.getDeclarationName(this));
String? get doc => documentationComment.formatted;
String? get type => doc == null
? null
: ParserRegexes.docTypeRegex.firstMatch(doc ?? '')?.group(1);
}T? tryOrNull<T>(T Function() f) {
try {
return f();
} catch (_) {
return null;
}
}There was a problem hiding this comment.
Refactored a bit:
List<ParameterDoc> parse() =>
File(
join(
ruleDirectory.path,
_parametersDir,
'${basename(ruleDirectory.path)}_$_parametersSuffix.dart',
),
).declarations
?.whereType<ClassDeclaration>()
.map(_parseParametersDocs)
.firstWhereOrNull((docs) => docs.isNotEmpty) ??
[];
List<ParameterDoc> _parseParametersDocs(ClassDeclaration declaration) => [
for (final member in ParserUtils.getClassMembers(
declaration,
))
if (member case FieldDeclaration(
isStatic: false,
documentationComment: Comment(formatted: final doc),
))
for (final v in member.fields.variables)
if (!v.nameString.startsWith('_'))
if (doc == null)
throw 'Documentation is not specified for field '
'"${v.nameString}" in class: '
'${ParserUtils.getDeclarationName(declaration)}'
else
ParameterDoc(
name: ParserUtils.camelCaseToSnakeCase(v.nameString),
type: (member.fields.type?.toSource() ?? 'dynamic')
.replaceAllMapped(ParserRegexes.wordRegex, (match) {
final word = match.group(0)!;
return customTypes[word] ?? word;
}),
doc: doc,
),
];extension on VariableDeclaration {
String get nameString => name.lexeme.trim();
}| static String _cleanCommentLine(String line) { | ||
| if (line.startsWith('/// ')) { | ||
| return line.substring(4); | ||
| } | ||
| if (line.startsWith('///')) { | ||
| return line.substring(3); | ||
| } | ||
| return line; | ||
| } |
There was a problem hiding this comment.
| static String _cleanCommentLine(String line) { | |
| if (line.startsWith('/// ')) { | |
| return line.substring(4); | |
| } | |
| if (line.startsWith('///')) { | |
| return line.substring(3); | |
| } | |
| return line; | |
| } | |
| static String _trimCommentPrefix(String line) => | |
| line.substring(switch (line) { | |
| _ when line.startsWith('/// ') => 4, | |
| _ when line.startsWith('///') => 3, | |
| _ => 0, | |
| }); |
There was a problem hiding this comment.
Also we can make an extension like trimChar or something
And make it:
static String _trimCommentPrefix(String line) => line.trimChar('/').trim();| static String? formatDocumentationComment(Comment? documentationComment) { | ||
| if (documentationComment == null) return null; | ||
| return documentationComment.tokens | ||
| .map((token) => _cleanCommentLine(token.lexeme)) | ||
| .join('\n') | ||
| .trim(); | ||
| } |
There was a problem hiding this comment.
| static String? formatDocumentationComment(Comment? documentationComment) { | |
| if (documentationComment == null) return null; | |
| return documentationComment.tokens | |
| .map((token) => _cleanCommentLine(token.lexeme)) | |
| .join('\n') | |
| .trim(); | |
| } | |
| static String? formatDocumentationComment(Comment? documentationComment) => | |
| documentationComment?.tokens | |
| .map((token) => _trimCommentPrefix(token.lexeme)) | |
| .join('\n') | |
| .trim(); |
| final name = match.group(1)!; | ||
| final rawContent = match.group(2)!; | ||
| final cleanContent = rawContent | ||
| .split('\n') | ||
| .map((line) => _cleanCommentLine(line.trim())) | ||
| .join('\n') | ||
| .trim(); | ||
|
|
||
| templates[name] = cleanContent; |
There was a problem hiding this comment.
I know it's in try catch, but we should also probably avoid the bang operator
| final name = match.group(1)!; | |
| final rawContent = match.group(2)!; | |
| final cleanContent = rawContent | |
| .split('\n') | |
| .map((line) => _cleanCommentLine(line.trim())) | |
| .join('\n') | |
| .trim(); | |
| templates[name] = cleanContent; | |
| final [name, rawContent] = match.groups([1, 2]); | |
| templates[name!] = rawContent! | |
| .split('\n') | |
| .map((line) => _trimCommentPrefix(line.trim())) | |
| .join('\n') | |
| .trim(); |
Closes #314
Closes #101