mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-21 07:21:46 +02:00
Merge faithful ripgrep type-case and file-only exclusions
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// --- Search flags ---
|
||||
|
||||
const POWERSHELL_SEARCHES = new Set(['get-childitem', 'gci', 'select-string', 'sls']);
|
||||
// Non-enumerable metadata preserves the existing string-array evidence shape.
|
||||
const RG_TYPE_FILTER_COUNT = Symbol('rg-type-filter-count');
|
||||
|
||||
const VALUE_FLAGS = new Set([
|
||||
'-e', '-f', '-g', '-t', '-T', '-m', '-A', '-B', '-C',
|
||||
@@ -203,15 +205,15 @@ function bracketClass(glob, open) {
|
||||
return { token: { type: 'class', negated, ranges }, end: i };
|
||||
}
|
||||
|
||||
function classMatches(token, ch) {
|
||||
function classMatches(token, ch, caseSensitive) {
|
||||
if (ch === '/') return false;
|
||||
const forms = [ch, ch.toLowerCase(), ch.toUpperCase()];
|
||||
const forms = caseSensitive ? [ch] : [ch, ch.toLowerCase(), ch.toUpperCase()];
|
||||
const member = token.ranges.some(([lo, hi]) => forms.some(form => form >= lo && form <= hi));
|
||||
return member !== token.negated;
|
||||
}
|
||||
|
||||
// see docs/gateguard/design-notes.md#glob-matching-without-regexp
|
||||
function wildcardMatch(glob, text) {
|
||||
function wildcardMatch(glob, text, caseSensitive) {
|
||||
const tokens = globTokens(glob);
|
||||
if (!tokens) return null;
|
||||
const n = text.length;
|
||||
@@ -224,7 +226,7 @@ function wildcardMatch(glob, text) {
|
||||
for (let j = n; j >= 0; j--) {
|
||||
if (token.type === 'lit') row[j] = j < n && text[j] === token.ch && next[j + 1];
|
||||
else if (token.type === 'one') row[j] = j < n && text[j] !== '/' && next[j + 1];
|
||||
else if (token.type === 'class') row[j] = j < n && classMatches(token, text[j]) && next[j + 1];
|
||||
else if (token.type === 'class') row[j] = j < n && classMatches(token, text[j], caseSensitive) && next[j + 1];
|
||||
else if (token.type === 'star') row[j] = next[j] || (j < n && text[j] !== '/' && row[j + 1]);
|
||||
else if (token.type === 'deep') row[j] = next[j] || (j < n && row[j + 1]);
|
||||
else {
|
||||
@@ -241,12 +243,12 @@ function normalizeFilterGlob(glob) {
|
||||
return String(glob).toLowerCase().replace(/\\/g, '/').replace(/^(?:\.\/)+/, '').replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function globMatches(glob, text, malformed) {
|
||||
function globMatches(glob, text, malformed, caseSensitive = false) {
|
||||
if (glob.length > MAX_FILTER_GLOB_CHARS) return null;
|
||||
const alternatives = expandBraces(glob);
|
||||
if (!alternatives) return null;
|
||||
return alternatives.some(alternative => {
|
||||
const matched = wildcardMatch(alternative, text);
|
||||
const matched = wildcardMatch(alternative, text, caseSensitive);
|
||||
return matched === null ? malformed : matched;
|
||||
});
|
||||
}
|
||||
@@ -273,8 +275,21 @@ function filtersAdmitTarget(item, ctx, stem) {
|
||||
const exclusions = Array.isArray(item.exclusions) ? item.exclusions : [];
|
||||
if (exclusions.length === 0) return true;
|
||||
if (exclusions.length > MAX_FILTERS_PER_SEARCH) return false;
|
||||
const typeStart = exclusions.length - (exclusions[RG_TYPE_FILTER_COUNT] || 0);
|
||||
const segments = targetSegments(ctx);
|
||||
return !exclusions.some(exclusion => exclusionCoversTarget(exclusion, segments, stem));
|
||||
return !exclusions.some((exclusion, index) => index >= typeStart
|
||||
? typeGlobMatchesTarget(exclusion, ctx, true) !== false
|
||||
: exclusionCoversTarget(exclusion, segments, stem));
|
||||
}
|
||||
|
||||
function typeGlobMatchesTarget(glob, ctx, exclusion = false) {
|
||||
const name = ctx.targetKey.split('/').pop() || '';
|
||||
// Windows evidence keys have lost filename case. Refuse inclusion credit and
|
||||
// conservatively check possible type exclusions, still only on the filename.
|
||||
if (/^[a-z]:\/|^\/\//i.test(ctx.targetKey)) {
|
||||
return exclusion ? globMatches(normalizeFilterGlob(glob), name.toLowerCase(), true) : null;
|
||||
}
|
||||
return globMatches(glob, name, false, true);
|
||||
}
|
||||
|
||||
function scopeRelativeSegments(item, ctx) {
|
||||
@@ -309,6 +324,7 @@ function includesAdmitTarget(item, ctx) {
|
||||
const includes = Array.isArray(item.includes) ? item.includes : [];
|
||||
if (includes.length === 0) return true;
|
||||
if (includes.length > MAX_FILTERS_PER_SEARCH) return false;
|
||||
if (includes[RG_TYPE_FILTER_COUNT]) return includes.some(glob => typeGlobMatchesTarget(glob, ctx) === true);
|
||||
return includes.some(include => includeAdmitsTarget(include, item, ctx));
|
||||
}
|
||||
|
||||
@@ -397,11 +413,16 @@ function resolveTypeFilters({ typeIncludes = [], typeExclusions = [], ...filters
|
||||
const hasIncludes = filters.includes.length > 0;
|
||||
return {
|
||||
...filters,
|
||||
includes: hasIncludes ? filters.includes : typeIncludes,
|
||||
exclusions: filters.exclusions.concat(hasIncludes ? [] : typeExclusions)
|
||||
includes: hasIncludes ? filters.includes : markTypeFilters(typeIncludes, typeIncludes.length),
|
||||
exclusions: markTypeFilters(filters.exclusions.concat(hasIncludes ? [] : typeExclusions), hasIncludes ? 0 : typeExclusions.length)
|
||||
};
|
||||
}
|
||||
|
||||
function markTypeFilters(globs, count) {
|
||||
if (!count) return globs;
|
||||
return Object.freeze(Object.defineProperty(globs.slice(), RG_TYPE_FILTER_COUNT, { value: count }));
|
||||
}
|
||||
|
||||
function shellSearchFilters(kind, args) {
|
||||
const filters = { exclusions: [], includes: [], opaque: false, dropped: new Set() };
|
||||
if (kind === 'find') return findSearchFilters(args, filters);
|
||||
|
||||
@@ -105,6 +105,47 @@ test('rg -T/--type-not excludes the type globs', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('rg type selection preserves case in literals and bracket classes', () => {
|
||||
const cpp = rgItem(['-t', 'cpp', 'tokenizer', 'src']);
|
||||
assert.strictEqual(admitted(cpp, 'src/tokenizer.c'), false);
|
||||
assert.strictEqual(admitted(cpp, 'src/cpp.C'), true);
|
||||
assert.strictEqual(admitted(cpp, 'src/tokenizer.h'), true);
|
||||
assert.strictEqual(admitted(cpp, 'src/tokenizer.H'), true);
|
||||
assert.strictEqual(admitted(cpp, 'src/tokenizer.CC'), false);
|
||||
assert.strictEqual(admitted(rgItem(['-t', 'js']), 'src/tokenizer.JS'), false);
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'cpp']), 'src/tokenizer.c'), true);
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'cpp']), 'src/tokenizer.C'), false);
|
||||
const grepCpp = { ...grepToolFilters(undefined, 'cpp'), scopes: [['.']], pathIncludes: true };
|
||||
assert.strictEqual(admitted(grepCpp, 'src/tokenizer.c'), false);
|
||||
assert.strictEqual(admitted(grepCpp, 'src/cpp.C'), true);
|
||||
});
|
||||
|
||||
test('rg negative types match only the file basename; path globs still exclude ancestors', () => {
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'js']), 'src.js/tokenizer.py'), true);
|
||||
assert.strictEqual(admitted(rgItem(['--type-not=cpp']), 'vendor.C/tokenizer.py'), true);
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'js']), 'src.py/tokenizer.js'), false);
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'js', '-g', '!vendor/**']), 'vendor/src.js/tokenizer.py'), false);
|
||||
assert.strictEqual(admitted(rgItem(['-g', '!*.js']), 'src.js/tokenizer.py'), false);
|
||||
assert.strictEqual(admitted(rgItem(['-T', 'js', '-g', '*.js']), 'src.js/tokenizer.js'), true);
|
||||
});
|
||||
|
||||
test('case-folded Windows contexts cannot prove type coverage or bypass a possible exclusion', () => {
|
||||
const ctx = file => ({ targetKey: `c:/proj/${file}`, resolveDir: () => 'c:/proj' });
|
||||
assert.strictEqual(includesAdmitTarget(rgItem(['-t', 'js']), ctx('src/tokenizer.js')), false);
|
||||
assert.strictEqual(filtersAdmitTarget(rgItem(['-T', 'cpp']), ctx('src/tokenizer.c'), null), false);
|
||||
assert.strictEqual(filtersAdmitTarget(rgItem(['-T', 'js']), ctx('src.js/tokenizer.py'), null), true);
|
||||
});
|
||||
|
||||
test('type filters keep the existing enumerable fields and string-array evidence shape', () => {
|
||||
for (const item of [shellSearchFilters('rg', ['-t', 'cpp']), shellSearchFilters('rg', ['-T', 'cpp', '-g', '!vendor/**'])]) {
|
||||
assert.deepStrictEqual(Object.keys(item).sort(), ['dropped', 'exclusions', 'includes', 'opaque']);
|
||||
assert.ok(item.includes.every(glob => typeof glob === 'string'));
|
||||
assert.ok(item.exclusions.every(glob => typeof glob === 'string'));
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(item.includes)), [...item.includes]);
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(item.exclusions)), [...item.exclusions]);
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit rg globs take precedence over type alternatives in either flag order', () => {
|
||||
for (const args of [['-t', 'js', '-g', '*.py'], ['-g', '*.py', '--type=js']]) {
|
||||
const item = rgItem([...args, 'tokenizer', 'src']);
|
||||
|
||||
Reference in New Issue
Block a user