@nitra/cursor 1.8.145 → 1.8.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,7 +285,7 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
285
285
  }
286
286
 
287
287
  const dir = dirname(abs)
288
- let iniNames = []
288
+ let iniNames
289
289
  try {
290
290
  const dirEntries = await readdir(dir)
291
291
  iniNames = dirEntries.filter(n => n.endsWith('.ini'))
@@ -77,4 +77,3 @@ export async function check() {
77
77
  await checkWorkflow(reporter)
78
78
  return reporter.getExitCode()
79
79
  }
80
-
@@ -40,7 +40,7 @@ const CSPELL_REQUIRED_IGNORE_PATHS = [
40
40
  '.vscode',
41
41
  'report',
42
42
  '*.svg',
43
- '**/k8s/**/*.yaml',
43
+ '**/k8s/**/*.yaml'
44
44
  ]
45
45
 
46
46
  /**
@@ -69,6 +69,42 @@ function isEsbuildScanFile(relPosix) {
69
69
  )
70
70
  }
71
71
 
72
+ /**
73
+ * Збирає `esbuild`-матчі по рядках одного файлу, поки буфер не досягне ліміту.
74
+ * @param {string} rel relative path
75
+ * @param {string} content вміст файлу
76
+ * @param {{ rel: string; line: number; snippet: string }[]} matches буфер для збору матчів
77
+ * @param {number} maxMatches максимум елементів у буфері
78
+ */
79
+ function appendEsbuildLineMatches(rel, content, matches, maxMatches) {
80
+ const lines = content.split('\n')
81
+ for (const [i, line] of lines.entries()) {
82
+ if (matches.length >= maxMatches) return
83
+ if (ESBUILD_RE.test(line)) {
84
+ matches.push({ rel, line: i + 1, snippet: line.trim() })
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Перебирає вибрані файли пакета і збирає до `maxMatches` згадок `esbuild`.
91
+ * @param {string} absPackageRoot абсолютний шлях до кореня пакета
92
+ * @param {{ rel: string }[]} files перелік відносних шляхів
93
+ * @param {number} maxMatches максимум знайдених матчів
94
+ * @returns {Promise<{ rel: string; line: number; snippet: string }[]>} зібрані матчі
95
+ */
96
+ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
97
+ /** @type {{ rel: string; line: number; snippet: string }[]} */
98
+ const matches = []
99
+ for (const { rel } of files) {
100
+ if (matches.length >= maxMatches) break
101
+ const content = await readFile(join(absPackageRoot, rel), 'utf8')
102
+ if (!ESBUILD_RE.test(content)) continue
103
+ appendEsbuildLineMatches(rel, content, matches, maxMatches)
104
+ }
105
+ return matches
106
+ }
107
+
72
108
  /**
73
109
  * Сканує дерево пакета на згадки `esbuild` і підказує заміну на `rolldown`.
74
110
  * @param {string} rootDir відносний шлях до пакета
@@ -78,31 +114,16 @@ function isEsbuildScanFile(relPosix) {
78
114
  * @param {(msg: string) => void} fail callback при помилці
79
115
  */
80
116
  async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fail) {
81
- /** @type {{ rel: string; line: number; snippet: string }[]} */
82
- const hits = []
83
-
117
+ /** @type {{ rel: string }[]} */
118
+ const candidates = []
84
119
  await walkDir(absPackageRoot, absPath => {
85
120
  const rel = relative(absPackageRoot, absPath).split('\\').join('/')
86
121
  if (!isEsbuildScanFile(rel)) return
87
- hits.push({ rel, line: 0, snippet: '' })
122
+ candidates.push({ rel })
88
123
  })
89
124
 
90
- // ми використали hits як буфер шляхів; зараз перетворимо на реальні співпадіння
91
- /** @type {{ rel: string; line: number; snippet: string }[]} */
92
- const matches = []
93
- for (const { rel } of hits) {
94
- const content = await readFile(join(absPackageRoot, rel), 'utf8')
95
- if (!ESBUILD_RE.test(content)) continue
96
-
97
- const lines = content.split('\n')
98
- for (let i = 0; i < lines.length; i++) {
99
- if (ESBUILD_RE.test(lines[i])) {
100
- matches.push({ rel, line: i + 1, snippet: lines[i].trim() })
101
- if (matches.length >= 30) break
102
- }
103
- }
104
- if (matches.length >= 30) break
105
- }
125
+ const maxMatches = 30
126
+ const matches = await collectEsbuildMatchesInFiles(absPackageRoot, candidates, maxMatches)
106
127
 
107
128
  if (matches.length === 0) {
108
129
  passFn(`${prefix}немає згадок 'esbuild' у джерелах пакета (очікується rolldown)`)
@@ -112,8 +133,8 @@ async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fai
112
133
  for (const m of matches) {
113
134
  fail(`${prefix}${m.rel}:${m.line} — знайдено 'esbuild'. Замінити на 'rolldown'. Фрагмент: ${m.snippet}`)
114
135
  }
115
- if (matches.length >= 30) {
116
- fail(`${prefix}показано перші 30 збігів 'esbuild' (замінити на 'rolldown')`)
136
+ if (matches.length >= maxMatches) {
137
+ fail(`${prefix}показано перші ${maxMatches} збігів 'esbuild' (замінити на 'rolldown')`)
117
138
  }
118
139
  }
119
140
 
@@ -308,44 +329,60 @@ async function checkVuePackage(rootDir, fail, passFn) {
308
329
  }
309
330
 
310
331
  /**
311
- * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
312
- * @returns {Promise<number>} 0 все OK, 1 — є проблеми
332
+ * Збирає корені пакетів, у яких у `dependencies` є `vue`.
333
+ * @param {string[]} roots усі корені пакетів monorepo
334
+ * @returns {Promise<string[]>} перелік пакетів з vue у dependencies
313
335
  */
314
- export async function check() {
315
- const reporter = createCheckReporter()
316
- const { pass, fail } = reporter
317
-
318
- const roots = await getMonorepoPackageRootDirs()
336
+ async function collectVueRoots(roots) {
319
337
  /** @type {string[]} */
320
338
  const vueRoots = []
321
339
  for (const r of roots) {
322
340
  const p = join(r, 'package.json')
323
- if (existsSync(p)) {
324
- const pkg = JSON.parse(await readFile(p, 'utf8'))
325
- if (pkg.dependencies?.vue) vueRoots.push(r)
326
- }
341
+ if (!existsSync(p)) continue
342
+ const pkg = JSON.parse(await readFile(p, 'utf8'))
343
+ if (pkg.dependencies?.vue) vueRoots.push(r)
327
344
  }
345
+ return vueRoots
346
+ }
328
347
 
329
- if (vueRoots.length > 0) {
330
- if (existsSync('.vscode/extensions.json')) {
331
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
332
- if (ext.recommendations?.includes('Vue.volar')) {
333
- pass('extensions.json містить Vue.volar')
334
- } else {
335
- fail('extensions.json не містить Vue.volar — додай до recommendations')
336
- }
337
- } else {
338
- fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
339
- }
348
+ /**
349
+ * Перевіряє наявність рекомендації `Vue.volar` у `.vscode/extensions.json`.
350
+ * @param {(msg: string) => void} pass pass callback
351
+ * @param {(msg: string) => void} fail fail callback
352
+ * @returns {Promise<void>}
353
+ */
354
+ async function checkVueVolarRecommendation(pass, fail) {
355
+ if (!existsSync('.vscode/extensions.json')) {
356
+ fail('.vscode/extensions.json не існує (для Vue-проєкту потрібна рекомендація Vue.volar)')
357
+ return
358
+ }
359
+ const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
360
+ if (ext.recommendations?.includes('Vue.volar')) {
361
+ pass('extensions.json містить Vue.volar')
340
362
  } else {
341
- pass('Vue.volar: пропущено repo немає пакетів з vue у dependencies)')
363
+ fail('extensions.json не містить Vue.volar додай до recommendations')
342
364
  }
365
+ }
366
+
367
+ /**
368
+ * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
369
+ * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
370
+ */
371
+ export async function check() {
372
+ const reporter = createCheckReporter()
373
+ const { pass, fail } = reporter
374
+
375
+ const roots = await getMonorepoPackageRootDirs()
376
+ const vueRoots = await collectVueRoots(roots)
343
377
 
344
378
  if (vueRoots.length === 0) {
379
+ pass('Vue.volar: пропущено (у repo немає пакетів з vue у dependencies)')
345
380
  pass('vue не знайдено в dependencies жодного пакета (перевірка vue пропущена)')
346
381
  return reporter.getExitCode()
347
382
  }
348
383
 
384
+ await checkVueVolarRecommendation(pass, fail)
385
+
349
386
  for (const r of vueRoots) {
350
387
  await checkVuePackage(r, fail, pass)
351
388
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Визначення, чи виконується поточний ESM-модуль як точка входу CLI, а не як import у тестах чи інших модулях.
3
3
  *
4
- * У Bun використовується `import.meta.main`; у Node — порівняння `import.meta.url` з `process.argv[1]`
5
- * після `resolve`, щоб `bun path/to/script.mjs` і `node path/to/script.mjs` коректно вважалися прямим запуском.
4
+ * Порівняння `import.meta.url` з `process.argv[1]` після `resolve`, щоб `bun path/to/script.mjs`
5
+ * і `node path/to/script.mjs` коректно вважалися прямим запуском.
6
6
  */
7
7
  import { resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
@@ -12,9 +12,6 @@ import { fileURLToPath } from 'node:url'
12
12
  * @returns {boolean} `true`, якщо файл запущено напряму; інакше `false`.
13
13
  */
14
14
  export function isRunAsCli() {
15
- if (import.meta.main === true) {
16
- return true
17
- }
18
15
  try {
19
16
  const entry = process.argv[1]
20
17
  if (!entry) {
@@ -108,7 +108,7 @@ async function runBunInstall(projectRoot) {
108
108
  } catch (error) {
109
109
  const exitCode = typeof error?.code === 'number' ? error.code : null
110
110
  if (exitCode !== null && exitCode !== 0) {
111
- throw new Error(`bun i завершився з кодом ${exitCode}`)
111
+ throw new Error(`bun i завершився з кодом ${exitCode}`, { cause: error })
112
112
  }
113
113
  throw error
114
114
  }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Спільні утиліти для AST-сканерів JS/TS на oxc-parser:
3
+ * вибір мови за розширенням, переклад зміщення в номер рядка, стиснення сніпета,
4
+ * обхід AST з предками, парсинг програми з безпечним поверненням `null`,
5
+ * розпізнавання типових вузлів (функцій, `*.join(...)`),
6
+ * робота з `TemplateLiteral` (текст quasis, контекст SQL-списку).
7
+ *
8
+ * Використовується файлами `bun-sql-scan.mjs`, `mssql-pool-scan.mjs` та іншими сканерами
9
+ * для уникнення дублювання boilerplate.
10
+ */
11
+ import { parseSync } from 'oxc-parser'
12
+
13
+ const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
14
+
15
+ /**
16
+ * Мова для Oxc за шляхом файлу (розширення).
17
+ * @param {string} filePath віртуальний або реальний шлях до файлу
18
+ * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
19
+ */
20
+ export function langFromPath(filePath) {
21
+ const lower = filePath.toLowerCase()
22
+ if (lower.endsWith('.tsx')) return 'tsx'
23
+ if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
24
+ if (lower.endsWith('.jsx')) return 'jsx'
25
+ return 'js'
26
+ }
27
+
28
+ /**
29
+ * Номер рядка (1-based) за зміщенням у буфері.
30
+ * @param {string} content повний текст файлу
31
+ * @param {number} offset байтове зміщення початку фрагмента
32
+ * @returns {number} номер рядка від 1
33
+ */
34
+ export function offsetToLine(content, offset) {
35
+ let line = 1
36
+ const n = Math.min(offset, content.length)
37
+ for (let i = 0; i < n; i++) {
38
+ if (content.codePointAt(i) === 10) line++
39
+ }
40
+ return line
41
+ }
42
+
43
+ /**
44
+ * Стискає пробіли для повідомлення про порушення.
45
+ * @param {string} s фрагмент коду
46
+ * @returns {string} скорочений однорядковий рядок
47
+ */
48
+ export function normalizeSnippet(s) {
49
+ return s.replaceAll(/\s+/gu, ' ').trim().slice(0, 180)
50
+ }
51
+
52
+ /**
53
+ * Чи є вузол функцією.
54
+ * @param {unknown} node AST node
55
+ * @returns {boolean} true, якщо це будь-який вузол-функція
56
+ */
57
+ export function isFunctionNode(node) {
58
+ return (
59
+ !!node &&
60
+ typeof node === 'object' &&
61
+ typeof node.type === 'string' &&
62
+ (node.type === 'FunctionDeclaration' ||
63
+ node.type === 'FunctionExpression' ||
64
+ node.type === 'ArrowFunctionExpression')
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
70
+ * @param {unknown} node поточний вузол
71
+ * @param {unknown[]} ancestors масив предків від кореня до parent
72
+ * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
73
+ * @returns {void}
74
+ */
75
+ export function walkAstWithAncestors(node, ancestors, visit) {
76
+ if (!node || typeof node !== 'object') return
77
+ if (Array.isArray(node)) {
78
+ for (const item of node) walkAstWithAncestors(item, ancestors, visit)
79
+ return
80
+ }
81
+
82
+ const rec = /** @type {Record<string, unknown>} */ (node)
83
+ if (typeof rec.type === 'string') {
84
+ visit(rec, ancestors)
85
+ ancestors = [...ancestors, rec]
86
+ }
87
+
88
+ for (const key of Object.keys(node)) {
89
+ if (key === 'parent') continue
90
+ const v = rec[key]
91
+ if (v && typeof v === 'object') {
92
+ walkAstWithAncestors(v, ancestors, visit)
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Парсить файл і повертає `program` або null, якщо є синтаксичні помилки чи виняток.
99
+ * @param {string} content вихідний код
100
+ * @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
101
+ * @returns {unknown | null} `result.program` або null, якщо парсинг не вдався
102
+ */
103
+ export function parseProgramOrNull(content, virtualPath) {
104
+ const lang = langFromPath(virtualPath || 'scan.ts')
105
+ let result
106
+ try {
107
+ result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
108
+ } catch {
109
+ return null
110
+ }
111
+ if (result.errors?.length) return null
112
+ return result.program
113
+ }
114
+
115
+ /**
116
+ * Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
117
+ * @param {unknown} node AST node
118
+ * @returns {boolean} true, якщо це CallExpression `*.join(...)`
119
+ */
120
+ export function isJoinCall(node) {
121
+ if (!node || node.type !== 'CallExpression') return false
122
+ const callee = node.callee
123
+ if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
124
+ const prop = callee.property
125
+ return !!prop && prop.type === 'Identifier' && prop.name === 'join'
126
+ }
127
+
128
+ /**
129
+ * Текст quasis у TemplateLiteral (без expressions).
130
+ * @param {unknown} template TemplateLiteral
131
+ * @returns {string} обʼєднаний raw-текст
132
+ */
133
+ export function templateQuasisText(template) {
134
+ if (!template || template.type !== 'TemplateLiteral') return ''
135
+ const quasis = template.quasis
136
+ if (!Array.isArray(quasis) || quasis.length === 0) return ''
137
+ let out = ''
138
+ for (const q of quasis) {
139
+ if (!q || typeof q !== 'object') continue
140
+ const value = q.value
141
+ if (!value || typeof value !== 'object') continue
142
+ if (typeof value.raw === 'string') out += value.raw
143
+ }
144
+ return out
145
+ }
146
+
147
+ /**
148
+ * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
149
+ * @param {unknown} template TemplateLiteral
150
+ * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
151
+ */
152
+ export function isSqlListContextTemplate(template) {
153
+ return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
154
+ }
@@ -15,112 +15,19 @@
15
15
  * Якщо файл не парситься / містить синтаксичні помилки — повертаємо порожній
16
16
  * результат: спочатку треба полагодити синтаксис, потім перезапустити перевірку.
17
17
  */
18
- import { parseSync } from 'oxc-parser'
18
+ import {
19
+ isFunctionNode,
20
+ isJoinCall,
21
+ isSqlListContextTemplate,
22
+ normalizeSnippet,
23
+ offsetToLine,
24
+ parseProgramOrNull,
25
+ walkAstWithAncestors
26
+ } from './ast-scan-utils.mjs'
19
27
 
20
28
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
21
- const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
22
29
  const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
23
30
 
24
- /**
25
- * Мова для Oxc за шляхом файлу (розширення).
26
- * @param {string} filePath віртуальний або реальний шлях до файлу
27
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
28
- */
29
- function langFromPath(filePath) {
30
- const lower = filePath.toLowerCase()
31
- if (lower.endsWith('.tsx')) return 'tsx'
32
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
33
- if (lower.endsWith('.jsx')) return 'jsx'
34
- return 'js'
35
- }
36
-
37
- /**
38
- * Номер рядка (1-based) за зміщенням у буфері.
39
- * @param {string} content повний текст файлу
40
- * @param {number} offset байтове зміщення початку фрагмента
41
- * @returns {number} номер рядка від 1
42
- */
43
- function offsetToLine(content, offset) {
44
- let line = 1
45
- const n = Math.min(offset, content.length)
46
- for (let i = 0; i < n; i++) {
47
- if (content.codePointAt(i) === 10) line++
48
- }
49
- return line
50
- }
51
-
52
- /**
53
- * Стискає пробіли для повідомлення про порушення.
54
- * @param {string} s фрагмент коду
55
- * @returns {string} скорочений однорядковий рядок
56
- */
57
- function normalizeSnippet(s) {
58
- return s.replaceAll(/\s+/gu, ' ').trim().slice(0, 180)
59
- }
60
-
61
- /**
62
- * Чи є вузол функцією.
63
- * @param {unknown} node AST node
64
- * @returns {boolean} true, якщо це будь-який вузол-функція
65
- */
66
- function isFunctionNode(node) {
67
- return (
68
- !!node &&
69
- typeof node === 'object' &&
70
- typeof node.type === 'string' &&
71
- (node.type === 'FunctionDeclaration' ||
72
- node.type === 'FunctionExpression' ||
73
- node.type === 'ArrowFunctionExpression')
74
- )
75
- }
76
-
77
- /**
78
- * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
79
- * @param {unknown} node поточний вузол
80
- * @param {unknown[]} ancestors масив предків від кореня до parent
81
- * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
82
- * @returns {void}
83
- */
84
- function walkAstWithAncestors(node, ancestors, visit) {
85
- if (!node || typeof node !== 'object') return
86
- if (Array.isArray(node)) {
87
- for (const item of node) walkAstWithAncestors(item, ancestors, visit)
88
- return
89
- }
90
-
91
- const rec = /** @type {Record<string, unknown>} */ (node)
92
- if (typeof rec.type === 'string') {
93
- visit(rec, ancestors)
94
- ancestors = [...ancestors, rec]
95
- }
96
-
97
- for (const key of Object.keys(node)) {
98
- if (key === 'parent') continue
99
- const v = rec[key]
100
- if (v && typeof v === 'object') {
101
- walkAstWithAncestors(v, ancestors, visit)
102
- }
103
- }
104
- }
105
-
106
- /**
107
- * Парсить файл та повертає program або null, якщо є синтаксичні помилки.
108
- * @param {string} content вихідний код
109
- * @param {string} virtualPath шлях для вибору `lang`
110
- * @returns {unknown | null} `result.program` або null
111
- */
112
- function parseProgramOrNull(content, virtualPath) {
113
- const lang = langFromPath(virtualPath || 'scan.ts')
114
- let result
115
- try {
116
- result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
117
- } catch {
118
- return null
119
- }
120
- if (result.errors?.length) return null
121
- return result.program
122
- }
123
-
124
31
  /**
125
32
  * Чи це `new SQL(...)` (Identifier callee з імʼям `SQL`).
126
33
  * @param {unknown} node AST node
@@ -152,47 +59,6 @@ function isUnsafeCallWithInterpolatedTemplate(node) {
152
59
  return Array.isArray(expressions) && expressions.length > 0
153
60
  }
154
61
 
155
- /**
156
- * Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
157
- * @param {unknown} node AST node
158
- * @returns {boolean} true, якщо це CallExpression `*.join(...)`
159
- */
160
- function isJoinCall(node) {
161
- if (!node || node.type !== 'CallExpression') return false
162
- const callee = node.callee
163
- if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
164
- const prop = callee.property
165
- return !!prop && prop.type === 'Identifier' && prop.name === 'join'
166
- }
167
-
168
- /**
169
- * Текст quasis у TemplateLiteral (без expressions).
170
- * @param {unknown} template TemplateLiteral
171
- * @returns {string} обʼєднаний raw-текст
172
- */
173
- function templateQuasisText(template) {
174
- if (!template || template.type !== 'TemplateLiteral') return ''
175
- const quasis = template.quasis
176
- if (!Array.isArray(quasis) || quasis.length === 0) return ''
177
- let out = ''
178
- for (const q of quasis) {
179
- if (!q || typeof q !== 'object') continue
180
- const value = q.value
181
- if (!value || typeof value !== 'object') continue
182
- if (typeof value.raw === 'string') out += value.raw
183
- }
184
- return out
185
- }
186
-
187
- /**
188
- * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
189
- * @param {unknown} template TemplateLiteral
190
- * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
191
- */
192
- function isSqlListContextTemplate(template) {
193
- return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
194
- }
195
-
196
62
  /**
197
63
  * Знаходить `new SQL(...)` всередині функцій (handler на кожен запит замість singleton).
198
64
  * @param {string} content вихідний код
@@ -278,7 +144,7 @@ export function findUnsafeBunSqlDynamicSqlListInText(content, virtualPath = 'sca
278
144
  * Скан по сирому тексту — без AST, щоб бути дешевим: викликається на кожному
279
145
  * JS/TS-файлі при зборі ознак для авто-детекту правил.
280
146
  * @param {string} content вміст файлу
281
- * @returns {boolean}
147
+ * @returns {boolean} true, якщо є імпорт sql або SQL з модуля bun
282
148
  */
283
149
  export function textHasBunSqlImport(content) {
284
150
  return BUN_SQL_IMPORT_RE.test(content)
@@ -11,45 +11,11 @@
11
11
  */
12
12
  import { parseSync } from 'oxc-parser'
13
13
 
14
+ import { langFromPath, offsetToLine } from './ast-scan-utils.mjs'
15
+
14
16
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
15
17
  const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
16
18
 
17
- /**
18
- * Мова для Oxc за шляхом файлу (розширення).
19
- * @param {string} filePath віртуальний або реальний шлях до файлу
20
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
21
- */
22
- function langFromPath(filePath) {
23
- const lower = filePath.toLowerCase()
24
- if (lower.endsWith('.tsx')) {
25
- return 'tsx'
26
- }
27
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
28
- return 'ts'
29
- }
30
- if (lower.endsWith('.jsx')) {
31
- return 'jsx'
32
- }
33
- return 'js'
34
- }
35
-
36
- /**
37
- * Номер рядка (1-based) за зміщенням у буфері.
38
- * @param {string} content повний текст файлу
39
- * @param {number} offset байтове зміщення початку фрагмента
40
- * @returns {number} номер рядка від 1
41
- */
42
- function offsetToLine(content, offset) {
43
- let line = 1
44
- const n = Math.min(offset, content.length)
45
- for (let i = 0; i < n; i++) {
46
- if (content.codePointAt(i) === 10) {
47
- line++
48
- }
49
- }
50
- return line
51
- }
52
-
53
19
  /**
54
20
  * Стискає пробіли для повідомлення про порушення.
55
21
  * @param {string} s фрагмент коду