diff --git a/scripts/benchmark-profanity.mjs b/scripts/benchmark-profanity.mjs index 8a4db31..5bc4631 100644 --- a/scripts/benchmark-profanity.mjs +++ b/scripts/benchmark-profanity.mjs @@ -44,6 +44,17 @@ function printResults(results) { const filter = createProfanityFilter(); const scanner = createProfanityScanner({ filter }); const input = (text) => ({ text, codePoints: Array.from(text) }); +const hintedEmptyInput = { + text: "", + codePoints: [], + hints: { + textLength: 0, + codePointLength: 0, + isEmpty: true, + hasAsciiOnly: true, + hasNonAscii: false, + }, +}; printResults([ bench( @@ -58,6 +69,7 @@ printResults([ bench("check loose match", () => filter.check(LOOSE_MATCH)), bench("check long late match", () => filter.check(LONG_LATE_MATCH)), bench("scanner check short clean", () => scanner.check(input(SHORT_CLEAN))), + bench("scanner check hinted empty", () => scanner.check(hintedEmptyInput)), bench("scanner check short match", () => scanner.check(input(SHORT_MATCH))), bench("scanner scan short match", () => scanner.scan(input(SHORT_MATCH))), bench("analyze short match", () => filter.analyze(SHORT_MATCH)), diff --git a/src/scanner.ts b/src/scanner.ts index c2c430e..d80576a 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -36,7 +36,10 @@ export function createProfanityScanner( const scanner: ProfanityScanner = { name: PROFANITY_FILTER_NAME, - check: (input) => activeFilter.check(input.text, matchOptions), + check: (input) => + allocationAware && input.hints?.textLength === 0 + ? false + : activeFilter.check(input.text, matchOptions), scan, }; diff --git a/src/types.ts b/src/types.ts index fcd01b3..762e7fa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,10 +54,26 @@ export interface ProfanityFilter extends ReadonlyProfanityFilter { export interface ProfanityScanInput { readonly text: string; readonly codePoints: readonly string[]; - readonly hints?: { - readonly textLength?: number; - readonly hasNonAscii?: boolean; - }; + readonly hints?: ProfanityScanHints; +} + +export interface ProfanityScanHints { + readonly textLength?: number; + readonly codePointLength?: number; + readonly isEmpty?: boolean; + readonly hasAsciiOnly?: boolean; + readonly hasNonAscii?: boolean; + readonly hasDigit?: boolean; + readonly digitCount?: number; + readonly hasAsciiLetter?: boolean; + readonly hasWhitespace?: boolean; + readonly hasPunctuation?: boolean; + readonly punctuationCount?: number; + readonly hasAtSign?: boolean; + readonly hasDot?: boolean; + readonly hasSlash?: boolean; + readonly hasColon?: boolean; + readonly hasPlus?: boolean; } export interface ProfanityScannerOptions { diff --git a/tests/scanner.spec.ts b/tests/scanner.spec.ts index bf99441..be2da0b 100644 --- a/tests/scanner.spec.ts +++ b/tests/scanner.spec.ts @@ -71,6 +71,21 @@ describe("profanity scanner adapter", () => { ).toBe(false); }); + it("uses shared text-length hints to skip package-owned empty checks", () => { + const filter = createProfanityFilter(["alpha"], []); + const checkSpy = vi.spyOn(filter, "check"); + const scanner = createProfanityScanner({ filter }); + + expect( + scanner.check({ + text: "", + codePoints: [], + hints: { textLength: 0, hasNonAscii: false }, + }), + ).toBe(false); + expect(checkSpy).not.toHaveBeenCalled(); + }); + it("streams ranges into a sink and supports early stop", () => { const filter = createProfanityFilter(["alpha", "beta"], []); const scanner = createProfanityScanner({ filter }); @@ -108,17 +123,26 @@ describe("profanity scanner adapter", () => { const analyze = vi.fn(() => [ Object.assign([0, 5] as [number, number], { mode: "strict" as const }), ]); + const check = vi.fn(() => true); const scanner = createProfanityScanner({ filter: { name: PROFANITY_FILTER_NAME, analyze, - check: () => true, + check, censor: (text) => String(text), }, }); const seen: TextCodePointRange[] = []; expect("allocationAware" in scanner).toBe(false); + expect( + scanner.check({ + text: "", + codePoints: [], + hints: { textLength: 0, hasNonAscii: false }, + }), + ).toBe(true); + expect(check).toHaveBeenCalledOnce(); expect( scanner.scan( { text: "alpha", codePoints: Array.from("alpha") },