1import * as _ from 'lodash-es' 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}) => { 19 let needleStr, falsePosA, isCaseSensNeedle = false, isWholeWord = false, regexPattern = null 20 if (_.isArray(needleObj)) { 21 const [first, second] = needleObj 23 if (_.isPlainObject(second) && (second.exceptions || second.skipPaths)) { 24 falsePosA = _.castArray(second.exceptions || []) 25 skipPathsA = second.skipPaths || [] 27 falsePosA = _.castArray(second) 29 if (skipPathsA.some((skip: string) => path.includes(skip))) return 30 if (_.isPlainObject(first) && first.regex) { 31 regexPattern = first.regex 35 } else if (_.isString(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) 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[]} 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 66 let searchContents = contents 67 _.each(falsePosA, (falsePosStr) => { 68 searchContents = replaceAll(searchContents, falsePosStr, '') 70 const regex = new RegExp(regexPattern) 71 const match = regex.exec(searchContents) 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 }) 78 let replacedContents = contents 79 if (isCaseInsens && !isCaseSensNeedle) { 80 replacedContents = replacedContents.toLowerCase() 81 needleStr = needleStr.toLowerCase() 82 falsePosA = _.map(falsePosA, _.toLower) 84 _.each(falsePosA, (falsePosStr) => { 85 replacedContents = replaceAll(replacedContents, falsePosStr, '') 90 const regex = new RegExp(`\\b${_.escapeRegExp(needleStr)}\\b`, isCaseSensNeedle ? '' : 'i') 91 const match = regex.exec(replacedContents) 93 idx = match?.index ?? -1 95 idx = replacedContents.indexOf(needleStr) 100 // Find the word/identifier containing the needle at the match location 101 const wordRegex = /\w+/g 102 let containingWord = needleStr 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) 112 return {path, needleStr, containingWord} 114 const surroundingText = replacedContents.substring(idx - 20, idx + 20) 115 throw new PtErr('searchHit', {path, needleStr, surroundingText})