blob: 5495a767e23f376ccad65b8a116f5a880be6e0ff [file] [edit]
// 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.
/// This library contains helper methods and generated methods for effecient
/// string searching using a combination of BMH and KMP.
///
/// Method bodies - including the "magic tables" are generated and will be
/// recreated when the tool
/// `pkg/analyzer/tool/generators/string_search_util_generator.dart`
/// is run.
///
/// With the implementation it can only search for ascii characters and
/// the generator will crash if trying to generate for non-ascii characters.
/// Matching against a string with non-ascii characters is fine.
/// This is not considered a defect, but a tradeoff.
///
/// Adding new things to search for can be done by adding a new class with
/// new static members with an annotation like
/// `@_GeneratedSearchData('myNeedle')`
library;
/// Searches for [needle] in [haystack] starting from [offset].
///
/// Expects [bmhTable] and [kmpTable] to be correctly precomputed const lists.
///
/// This is a combination of Boyer–Moore–Horspool (BMH)
/// (https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm)
/// and Knuth–Morris–Pratt (KMP)
/// (https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm)
/// where BMH often has sub-linear performance by skipping (up to) needle-size
/// chunks when the last character doesn't match (but is worse-case O(n*m)) and
/// KMP runs in worst-case O(n) (with n being the length of [haystack] and m
/// being the length of [needle]).
int _combinedBmhAndKmp(
List<int> bmhTable,
List<int> kmpTable,
String haystack,
String needle,
int offset,
) {
int skip = offset;
int lastNeedleChar = needle.codeUnitAt(needle.length - 1);
outerLoop:
while (haystack.length - skip >= needle.length) {
// BMH: Try to match the last char: If it doesn't match we can skip
// something.
int haystackChar = haystack.codeUnitAt(skip + needle.length - 1);
while (lastNeedleChar != haystackChar) {
if (haystackChar < bmhTable.length) {
skip += bmhTable[haystackChar];
} else {
skip += needle.length;
}
int nextIndex = skip + needle.length - 1;
if (nextIndex >= haystack.length) return -1;
haystackChar = haystack.codeUnitAt(nextIndex);
}
// Matches on the last char. We switch to KMP.
int k = 0;
while (skip < haystack.length) {
if (needle.codeUnitAt(k) == haystack.codeUnitAt(skip)) {
skip++;
k++;
if (k == needle.length) {
return skip - k;
}
} else {
k = kmpTable[k];
if (k < 0) {
skip++;
// Here KMP would set k to 0 and thus start over searching from
// scratch at position skip in haystack. We instead continue with BMH.
continue outerLoop;
}
}
}
}
return -1;
}
class DartdocDirectiveSearchHelper {
// Don't instantiate.
DartdocDirectiveSearchHelper._();
@_GeneratedSearchData('{@endtemplate}', addNeedleLength: true)
static int nextAtEndtemplate(String haystack, int offset) {
const String needle = '{@endtemplate}';
const List<int> bmhTable = [
// Format hack.
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
12, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14,
14, 3, 14, 14, 9, 1, 14, 14,
14, 14, 14, 14, 4, 6, 10, 14,
5, 14, 14, 14, 2, 14, 14, 14,
14, 14, 14, 13, 14, 14, 14, 14,
];
const List<int> kmpTable = [
// Format hack.
-1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
];
int result = _combinedBmhAndKmp(
bmhTable,
kmpTable,
haystack,
needle,
offset,
);
if (result >= 0) result += needle.length;
return result;
}
@_GeneratedSearchData('{@template', addNeedleLength: true)
static int nextAtTemplate(String haystack, int offset) {
const String needle = '{@template';
const List<int> bmhTable = [
// Format hack.
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
8, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 10, 10, 10,
10, 2, 10, 10, 10, 6, 10, 10,
10, 10, 10, 10, 3, 5, 10, 10,
4, 10, 10, 10, 1, 10, 10, 10,
10, 10, 10, 9, 10, 10, 10, 10,
];
const List<int> kmpTable = [
// Format hack.
-1, 0, 0, 0, 0, 0, 0, 0,
0, 0,
];
int result = _combinedBmhAndKmp(
bmhTable,
kmpTable,
haystack,
needle,
offset,
);
if (result >= 0) result += needle.length;
return result;
}
}
class _GeneratedSearchData {
final String needle;
final bool addNeedleLength;
const _GeneratedSearchData(this.needle, {this.addNeedleLength = false});
}