| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io' as io; | |
| import 'package:analysis_server_client/protocol.dart' hide Element; | |
| import 'package:analysis_server_client/server.dart'; | |
| import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; | |
| import 'package:analyzer/dart/analysis/results.dart'; | |
| import 'package:analyzer/src/dart/ast/ast.dart'; | |
| import 'package:analyzer/src/dart/element/element.dart'; | |
| import 'package:analyzer_testing/package_root.dart' as pkg_root; | |
| import 'package:path/path.dart'; | |
| Future<void> main() async { | |
| var generator = StringSearchUtilGenerator(); | |
| var code = await generator.generate(); | |
| io.File(generator.filePath).writeAsStringSync(code); | |
| } | |
| /// Adapted from | |
| /// https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm#Description | |
| List<int> _bmhTable(String needle, {int tableSize = 256}) { | |
| List<int> result = List.filled(tableSize, needle.length); | |
| for (int i = 0; i < needle.length - 1; i++) { | |
| result[needle.codeUnitAt(i)] = needle.length - 1 - i; | |
| } | |
| return result; | |
| } | |
| /// Adapted from | |
| /// https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm#Description_of_pseudocode_for_the_table-building_algorithm | |
| /// | |
| /// This gives comparable results to the one given in | |
| /// "Introduction to Algorithms (2nd edition)" 32.4 (with the addition from | |
| /// exercice 32.4-4), except for the distinction between 0 and -1 that this | |
| /// version offers. | |
| List<int> _kmpTable(String needle) { | |
| int pos = 1; | |
| // the zero-based index in needle of the next character of the current | |
| // candidate substring | |
| int cnd = 0; | |
| List<int> result = List.filled(needle.length, 0); | |
| result[0] = -1; | |
| int needleLength = needle.length; | |
| while (pos < needleLength) { | |
| int needleAtPos = needle.codeUnitAt(pos); | |
| if (needleAtPos == needle.codeUnitAt(cnd)) { | |
| result[pos] = result[cnd]; | |
| } else { | |
| result[pos] = cnd; | |
| while (cnd >= 0 && needleAtPos != needle.codeUnitAt(cnd)) { | |
| cnd = result[cnd]; | |
| } | |
| } | |
| pos++; | |
| cnd++; | |
| } | |
| return result; | |
| } | |
| class StringSearchUtilGenerator { | |
| String get filePath { | |
| var analyzerPath = normalize(join(pkg_root.packageRoot, 'analyzer')); | |
| var analyzerLibPath = normalize(join(analyzerPath, 'lib')); | |
| return normalize( | |
| join(analyzerLibPath, 'src', 'utilities', 'string_search.dart'), | |
| ); | |
| } | |
| Future<String> generate() async { | |
| ResolvedUnitResult resolvedUnit = await _getAstResolvedUnit(); | |
| String newCode = resolvedUnit.content; | |
| var methods = _findMethods(resolvedUnit); | |
| newCode = _replaceMethods(newCode, methods); | |
| newCode = await _formatSortCode(filePath, newCode); | |
| return newCode; | |
| } | |
| List<MethodDeclarationImpl> _findMethods(ResolvedUnitResult resolvedUnit) { | |
| List<MethodDeclarationImpl> result = []; | |
| var declarations = resolvedUnit | |
| .unit | |
| .declarations; // ignore: analyzer_to_be_deprecated_use, unnecessary_ignore | |
| for (var nodeImpl in declarations) { | |
| if (nodeImpl is ClassDeclarationImpl) { | |
| for (var member in nodeImpl.body.members) { | |
| if (member is! MethodDeclarationImpl) continue; | |
| var method = member.declaredFragment?.element; | |
| if (method is! MethodElementImpl) continue; | |
| var annotationData = _getAnnotationData(method); | |
| if (annotationData == null) continue; | |
| result.add(member); | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| String _formatListElements(List<int> list) { | |
| StringBuffer sb = StringBuffer(); | |
| for (int i = 0; i < list.length; i++) { | |
| sb.write(list[i]); | |
| sb.write(","); | |
| if (i % 8 == 7) sb.write("\n"); | |
| } | |
| return sb.toString(); | |
| } | |
| ({String needle, bool addNeedleLength})? _getAnnotationData( | |
| MethodElementImpl method, | |
| ) { | |
| for (var annotation in method.metadata.annotations) { | |
| var value = annotation.computeConstantValue(); | |
| if (value == null) continue; | |
| var valueType = value.type; | |
| if (valueType == null) continue; | |
| if (valueType.element?.name != '_GeneratedSearchData') continue; | |
| var needle = value.getField('needle'); | |
| if (needle == null) continue; | |
| if (!(needle.type?.isDartCoreString ?? false)) continue; | |
| var addNeedleLength = value.getField('addNeedleLength'); | |
| if (addNeedleLength == null) continue; | |
| if (!(addNeedleLength.type?.isDartCoreBool ?? false)) continue; | |
| var needleString = needle.toStringValue(); | |
| var addNeedleLengthBool = addNeedleLength.toBoolValue(); | |
| if (needleString != null && addNeedleLengthBool != null) { | |
| return (needle: needleString, addNeedleLength: addNeedleLengthBool); | |
| } | |
| } | |
| return null; | |
| } | |
| Future<ResolvedUnitResult> _getAstResolvedUnit() async { | |
| var collection = AnalysisContextCollection(includedPaths: [filePath]); | |
| var analysisContext = collection.contextFor(filePath); | |
| var analysisSession = analysisContext.currentSession; | |
| var astUnitResult = await analysisSession.getResolvedUnit(filePath); | |
| return astUnitResult as ResolvedUnitResult; | |
| } | |
| String _replaceMethods(String newCode, List<MethodDeclarationImpl> methods) { | |
| var replacements = <_Replacement>[]; | |
| for (var method in methods) { | |
| var element = method.declaredFragment?.element as MethodElementImpl; | |
| var annotationData = _getAnnotationData(element)!; | |
| var bmhTable = _bmhTable(annotationData.needle, tableSize: 128); | |
| var kmpTable = _kmpTable(annotationData.needle); | |
| String addNeedleString = ""; | |
| if (annotationData.addNeedleLength) { | |
| addNeedleString = "if (result >= 0) result += needle.length;"; | |
| } | |
| String replaceWith = | |
| """ | |
| static int ${element.name}(String haystack, int offset) { | |
| const String needle = '${annotationData.needle}'; | |
| const List<int> bmhTable = [ | |
| // Format hack. | |
| ${_formatListElements(bmhTable)} | |
| ]; | |
| const List<int> kmpTable = [ | |
| // Format hack. | |
| ${_formatListElements(kmpTable)} | |
| ]; | |
| int result = _combinedBmhAndKmp(bmhTable, kmpTable, haystack, | |
| needle, offset); | |
| $addNeedleString | |
| return result; | |
| }"""; | |
| replacements.add( | |
| _Replacement( | |
| method.firstTokenAfterCommentAndMetadata.offset, | |
| method.end, | |
| replaceWith, | |
| ), | |
| ); | |
| } | |
| replacements.sort((a, b) => b.offset - a.offset); | |
| for (var replacement in replacements) { | |
| newCode = | |
| newCode.substring(0, replacement.offset) + | |
| replacement.text + | |
| newCode.substring(replacement.end); | |
| } | |
| return newCode; | |
| } | |
| static Future<String> _formatSortCode(String path, String code) async { | |
| var server = Server(); | |
| await server.start(); | |
| server.listenToOutput(); | |
| await server.send('analysis.setAnalysisRoots', { | |
| 'included': [path], | |
| 'excluded': [], | |
| }); | |
| Future<void> updateContent() async { | |
| await server.send('analysis.updateContent', { | |
| 'files': { | |
| path: {'type': 'add', 'content': code}, | |
| }, | |
| }); | |
| } | |
| await updateContent(); | |
| var formatResponse = await server.send('edit.format', { | |
| 'file': path, | |
| 'selectionOffset': 0, | |
| 'selectionLength': code.length, | |
| }); | |
| var formatResult = EditFormatResult.fromJson( | |
| ResponseDecoder(null), | |
| 'result', | |
| formatResponse, | |
| ); | |
| code = SourceEdit.applySequence(code, formatResult.edits); | |
| await updateContent(); | |
| var sortResponse = await server.send('edit.sortMembers', {'file': path}); | |
| var sortResult = EditSortMembersResult.fromJson( | |
| ResponseDecoder(null), | |
| 'result', | |
| sortResponse, | |
| ); | |
| code = SourceEdit.applySequence(code, sortResult.edit.edits); | |
| await server.kill(); | |
| return code; | |
| } | |
| } | |
| class _Replacement { | |
| final int offset; | |
| final int end; | |
| final String text; | |
| _Replacement(this.offset, this.end, this.text); | |
| } |