🌳
pt0/deployF/codeSearchF/perFileGuardNeedleF.mts
1import * as _ from 'lodash-es'
6// Needle formats:
7// 'string' - substring match (case-insensitive)
8// ['needle', 'exception'] - substring with false-positive exceptions
9// [{regex: 'pattern'}, 'exception'] - regex with false-positive exceptions
10// [{regex: 'pattern'}, {exceptions: [...], skipPaths: [...]}] - regex with exceptions + path skips
11// { caseSens: 'string' } - case-sensitive substring (legacy)
12// { wholeWord: 'string' } - whole-word case-insensitive (legacy)
13// { needle: 'str', caseSens?: bool, wholeWord?: bool } - combined flags
14// { regex: 'pattern' } - regex match on contents (case-sensitive)
15// { filenameRegex: 'pattern' } - regex match on filename only
16// eslint-disable-next-line @typescript-eslint/no-explicit-any
17export const perFileGuardNeedle = ({isCaseInsens=true, needleStrA, raiseOnFind=true, path, contents}: {isCaseInsens?: boolean, needleStrA: any[], raiseOnFind?: boolean, path: string, contents: string}) => {
18 return find1Ret(needleStrA, (needleObj) => {
19 let needleStr, falsePosA, isCaseSensNeedle = false, isWholeWord = false, regexPattern = null
20 if (_.isArray(needleObj)) {
21 const [first, second] = needleObj
22 let skipPathsA = []
23 if (_.isPlainObject(second) && (second.exceptions || second.skipPaths)) {
24 falsePosA = _.castArray(second.exceptions || [])
25 skipPathsA = second.skipPaths || []
26 } else {
27 falsePosA = _.castArray(second)
28 }
29 if (skipPathsA.some((skip: string) => path.includes(skip))) return
30 if (_.isPlainObject(first) && first.regex) {
31 regexPattern = first.regex
32 } else {
33 needleStr = first
34 }
35 } else if (_.isString(needleObj)) {
36 needleStr = needleObj
37 } else if (_.isPlainObject(needleObj) && needleObj.regex) {
38 regexPattern = needleObj.regex
39 } else if (_.isPlainObject(needleObj) && needleObj.filenameRegex) {
40 const filename = path.split('/').pop() || ''
41 const regex = new RegExp(needleObj.filenameRegex)
42 const match = regex.exec(filename)
43 if (!match) return
44 if (!raiseOnFind) return { path, needleStr: needleObj.filenameRegex, containingWord: filename }
45 throw new PtErr('searchHit', { path, needleStr: needleObj.filenameRegex, surroundingText: filename })
46 } else if (_.isPlainObject(needleObj) && needleObj.needle) {
47 // Combined flags format: { needle: 'str', caseSens?: bool, wholeWord?: bool, exceptions?: [...] }
48 const no = needleObj as {needle: string, caseSens?: boolean, wholeWord?: boolean, exceptions?: string[]}
49 needleStr = no.needle
50 isCaseSensNeedle = !!no.caseSens
51 isWholeWord = !!no.wholeWord
52 falsePosA = no.exceptions
53 } else if (_.isPlainObject(needleObj) && (needleObj as {caseSens?: string}).caseSens) {
54 // Legacy: { caseSens: 'string' }
55 needleStr = (needleObj as {caseSens: string}).caseSens
56 isCaseSensNeedle = true
57 } else if (_.isPlainObject(needleObj) && (needleObj as {wholeWord?: string}).wholeWord) {
58 // Legacy: { wholeWord: 'string' }
59 needleStr = (needleObj as {wholeWord: string}).wholeWord
60 isWholeWord = true
61 } else {
62 throPtErr('2jf093j29fj', {needleObj})
63 }
65 if (regexPattern) {
66 let searchContents = contents
67 _.each(falsePosA, (falsePosStr) => {
68 searchContents = replaceAll(searchContents, falsePosStr, '')
69 })
70 const regex = new RegExp(regexPattern)
71 const match = regex.exec(searchContents)
72 if (!match) return
73 const matchedText = match[0]
74 if (!raiseOnFind) return { path, needleStr: regexPattern, containingWord: matchedText }
75 const surroundingText = searchContents.substring(match.index - 20, match.index + 20)
76 throw new PtErr('searchHit', { path, needleStr: regexPattern, surroundingText })
77 }
78 let replacedContents = contents
79 if (isCaseInsens && !isCaseSensNeedle) {
80 replacedContents = replacedContents.toLowerCase()
81 needleStr = needleStr.toLowerCase()
82 falsePosA = _.map(falsePosA, _.toLower)
83 }
84 _.each(falsePosA, (falsePosStr) => {
85 replacedContents = replaceAll(replacedContents, falsePosStr, '')
86 })
88 let isHit, idx
89 if (isWholeWord) {
90 const regex = new RegExp(`\\b${_.escapeRegExp(needleStr)}\\b`, isCaseSensNeedle ? '' : 'i')
91 const match = regex.exec(replacedContents)
92 isHit = !!match
93 idx = match?.index ?? -1
94 } else {
95 idx = replacedContents.indexOf(needleStr)
96 isHit = idx !== -1
97 }
99 if (!isHit) return
100 // Find the word/identifier containing the needle at the match location
101 const wordRegex = /\w+/g
102 let containingWord = needleStr
103 let match
104 while ((match = wordRegex.exec(replacedContents)) !== null) {
105 if (match.index <= idx && idx < match.index + match[0].length) {
106 // Found word spanning the match - get original casing from contents
107 containingWord = contents.substring(match.index, match.index + match[0].length)
108 break
109 }
110 }
111 if (!raiseOnFind) {
112 return {path, needleStr, containingWord}
113 }
114 const surroundingText = replacedContents.substring(idx - 20, idx + 20)
115 throw new PtErr('searchHit', {path, needleStr, surroundingText})
116 })