@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.
@@ -22,95 +22,20 @@
22
22
  */
23
23
  import { parseSync } from 'oxc-parser'
24
24
 
25
+ import {
26
+ isFunctionNode,
27
+ isJoinCall,
28
+ isSqlListContextTemplate,
29
+ langFromPath,
30
+ normalizeSnippet,
31
+ offsetToLine,
32
+ walkAstWithAncestors
33
+ } from './ast-scan-utils.mjs'
34
+
25
35
  const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
26
- const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
27
36
  const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
28
37
  const NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])
29
38
 
30
- /**
31
- * Мова для Oxc за шляхом файлу (розширення).
32
- * @param {string} filePath віртуальний або реальний шлях до файлу
33
- * @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
34
- */
35
- function langFromPath(filePath) {
36
- const lower = filePath.toLowerCase()
37
- if (lower.endsWith('.tsx')) return 'tsx'
38
- if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
39
- if (lower.endsWith('.jsx')) return 'jsx'
40
- return 'js'
41
- }
42
-
43
- /**
44
- * Номер рядка (1-based) за зміщенням у буфері.
45
- * @param {string} content повний текст файлу
46
- * @param {number} offset байтове зміщення початку фрагмента
47
- * @returns {number} номер рядка від 1
48
- */
49
- function offsetToLine(content, offset) {
50
- let line = 1
51
- const n = Math.min(offset, content.length)
52
- for (let i = 0; i < n; i++) {
53
- if (content.codePointAt(i) === 10) line++
54
- }
55
- return line
56
- }
57
-
58
- /**
59
- * Стискає пробіли для повідомлення про порушення.
60
- * @param {string} s фрагмент коду
61
- * @returns {string} скорочений однорядковий рядок
62
- */
63
- function normalizeSnippet(s) {
64
- return s.replaceAll(/\s+/g, ' ').trim().slice(0, 180)
65
- }
66
-
67
- /**
68
- * Чи є вузол функцією.
69
- * @param {unknown} node AST node
70
- * @returns {boolean} true, якщо це будь-який вузол-функція
71
- */
72
- function isFunctionNode(node) {
73
- return (
74
- !!node &&
75
- typeof node === 'object' &&
76
- typeof node.type === 'string' &&
77
- (node.type === 'FunctionDeclaration' ||
78
- node.type === 'FunctionExpression' ||
79
- node.type === 'ArrowFunctionExpression')
80
- )
81
- }
82
-
83
- /**
84
- * Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
85
- * @param {unknown} node поточний вузол
86
- * @param {unknown[]} ancestors масив предків від кореня до parent
87
- * @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
88
- * @returns {void}
89
- */
90
- function walkAstWithAncestors(node, ancestors, visit) {
91
- if (!node || typeof node !== 'object') return
92
- if (Array.isArray(node)) {
93
- for (const item of node) walkAstWithAncestors(item, ancestors, visit)
94
- return
95
- }
96
-
97
- const rec = /** @type {Record<string, unknown>} */ (node)
98
- if (typeof rec.type === 'string') {
99
- visit(rec, ancestors)
100
- ancestors = [...ancestors, rec]
101
- }
102
-
103
- for (const key of Object.keys(node)) {
104
- if (key === 'parent') {
105
- continue
106
- }
107
- const v = rec[key]
108
- if (v && typeof v === 'object') {
109
- walkAstWithAncestors(v, ancestors, visit)
110
- }
111
- }
112
- }
113
-
114
39
  /**
115
40
  * Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
116
41
  * @param {unknown} node AST node
@@ -160,48 +85,6 @@ function isRequestFactoryCall(node) {
160
85
  return !!prop && prop.type === 'Identifier' && prop.name === 'request'
161
86
  }
162
87
 
163
- /**
164
- * Чи це `.join(...)` виклик (часто використовується для динамічних списків в SQL).
165
- * @param {unknown} node AST node
166
- * @returns {boolean} true, якщо це CallExpression `*.join(...)`
167
- */
168
- function isJoinCall(node) {
169
- if (!node || node.type !== 'CallExpression') return false
170
- const callee = node.callee
171
- if (!callee || callee.type !== 'MemberExpression') return false
172
- if (callee.computed) return false
173
- const prop = callee.property
174
- return !!prop && prop.type === 'Identifier' && prop.name === 'join'
175
- }
176
-
177
- /**
178
- * Повертає текст quasis у TemplateLiteral (без expressions).
179
- * @param {unknown} template TemplateLiteral
180
- * @returns {string} обʼєднаний текст
181
- */
182
- function templateQuasisText(template) {
183
- if (!template || template.type !== 'TemplateLiteral') return ''
184
- const quasis = template.quasis
185
- if (!Array.isArray(quasis) || quasis.length === 0) return ''
186
- let out = ''
187
- for (const q of quasis) {
188
- if (!q || typeof q !== 'object') continue
189
- const value = q.value
190
- if (!value || typeof value !== 'object') continue
191
- if (typeof value.raw === 'string') out += value.raw
192
- }
193
- return out
194
- }
195
-
196
- /**
197
- * Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
198
- * @param {unknown} template TemplateLiteral
199
- * @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
200
- */
201
- function isSqlListContextTemplate(template) {
202
- return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
203
- }
204
-
205
88
  /**
206
89
  * Знаходить створення `ConnectionPool` всередині функцій.
207
90
  * @param {string} content вихідний код
@@ -307,7 +190,6 @@ export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
307
190
  *
308
191
  * Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
309
192
  * потрапляє “готовий шматок SQL”, а не параметризовані значення.
310
- *
311
193
  * @param {string} content вихідний код
312
194
  * @param {string} [virtualPath] шлях для вибору `lang`
313
195
  * @returns {{ line: number, snippet: string }[]} список порушень
@@ -371,6 +253,24 @@ function isLiteralNumericArrayExpression(node) {
371
253
  })
372
254
  }
373
255
 
256
+ /**
257
+ * Чи це безпосередній виклик числового парсера (parseInt/parseFloat/Number/BigInt)
258
+ * або обʼєктний доступ до них (наприклад `Number.parseInt(...)`).
259
+ * @param {Record<string, unknown>} node AST CallExpression
260
+ * @returns {boolean} true, якщо callee — числовий парсер
261
+ */
262
+ function isNumericParseCallExpression(node) {
263
+ if (node.type !== 'CallExpression') return false
264
+ const callee = node.callee
265
+ if (!callee) return false
266
+ if (callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) return true
267
+ if (callee.type === 'MemberExpression' && !callee.computed) {
268
+ const prop = callee.property
269
+ return !!prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)
270
+ }
271
+ return false
272
+ }
273
+
374
274
  /**
375
275
  * Чи містить піддерево виклик числового парсера (parseInt/parseFloat/Number/BigInt)
376
276
  * або унарний `+` (приведення до Number). Це сигнал, що значення гарантовано числове
@@ -380,20 +280,9 @@ function isLiteralNumericArrayExpression(node) {
380
280
  */
381
281
  function subtreeHasNumericParseCall(node) {
382
282
  if (!node || typeof node !== 'object') return false
383
- if (Array.isArray(node)) return node.some(subtreeHasNumericParseCall)
283
+ if (Array.isArray(node)) return node.some(item => subtreeHasNumericParseCall(item))
384
284
 
385
- if (node.type === 'CallExpression') {
386
- const callee = node.callee
387
- if (callee && callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) {
388
- return true
389
- }
390
- if (callee && callee.type === 'MemberExpression' && !callee.computed) {
391
- const prop = callee.property
392
- if (prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)) {
393
- return true
394
- }
395
- }
396
- }
285
+ if (isNumericParseCallExpression(node)) return true
397
286
  if (node.type === 'UnaryExpression' && node.operator === '+') return true
398
287
 
399
288
  for (const key of Object.keys(node)) {
@@ -426,7 +315,6 @@ function collectVariableDeclarators(programNode) {
426
315
  *
427
316
  * Якщо для Identifier немає видимого init (наприклад параметр функції чи import),
428
317
  * вираз вважається не парсованим — потрібен явний парсер на місці підстановки.
429
- *
430
318
  * @param {unknown} expr вираз з template.expressions
431
319
  * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и файлу
432
320
  * @param {Set<string>} [seen] іменa Identifier-ів, що вже трасуються (анти-цикл)
@@ -444,13 +332,7 @@ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
444
332
  const inits = declarators
445
333
  .filter(d => {
446
334
  const id = d.id
447
- return (
448
- !!id &&
449
- typeof id === 'object' &&
450
- id.type === 'Identifier' &&
451
- id.name === expr.name &&
452
- !!d.init
453
- )
335
+ return !!id && typeof id === 'object' && id.type === 'Identifier' && id.name === expr.name && !!d.init
454
336
  })
455
337
  .map(d => d.init)
456
338
  if (inits.length === 0) return false
@@ -461,19 +343,52 @@ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
461
343
  }
462
344
 
463
345
  /**
464
- * Знаходить підстановки `IN (${expr})` у TemplateLiteral, де `expr` не пройшов числовий парсер.
465
- *
466
- * Навіть у безпечному `pool.request().query\`...\`` краще явно парсити значення (parseInt/
467
- * Number/BigInt/parseFloat) та фільтрувати NaN — це гарантує, що жодний елемент не може
468
- * містити SQL-метасимволи, навіть якщо колись query-функція або обгортка зміняться. У
469
- * небезпечних контекстах (наприклад `pool.query(String.raw\`...\`)`) це єдиний бар'єр від SQL
470
- * injection.
471
- *
472
- * Випадки `${arr.join(',')}` свідомо ігноруються — їх ловить
473
- * {@link findUnsafeMssqlDynamicSqlListInText}.
346
+ * Сирий текст quasi-елемента TemplateLiteral на позиції перед expressions[i].
347
+ * @param {unknown} q quasi-елемент TemplateLiteral
348
+ * @returns {string} `q.value.raw` або порожній рядок, якщо структура не підходить
349
+ */
350
+ function quasiRawText(q) {
351
+ return q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
352
+ ? q.value.raw
353
+ : ''
354
+ }
355
+
356
+ /**
357
+ * Збирає порушення для одного TemplateLiteral вузла: знаходить expressions, що
358
+ * стоять одразу після `IN (` без числового парсера значень.
359
+ * @param {Record<string, unknown>} node TemplateLiteral
360
+ * @param {string} content вихідний код
361
+ * @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и для трасування
362
+ * @param {{ line: number, snippet: string }[]} out буфер результатів
363
+ */
364
+ function collectInListUnparsedFromTemplate(node, content, declarators, out) {
365
+ if (node.type !== 'TemplateLiteral') return
366
+ const quasis = node.quasis
367
+ const expressions = node.expressions
368
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
369
+
370
+ for (const [i, expr] of expressions.entries()) {
371
+ if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
372
+ if (!expr || typeof expr !== 'object') continue
373
+ if (isJoinCall(expr)) continue
374
+ if (isInListExpressionParsed(expr, declarators)) continue
375
+
376
+ const startOffset = typeof expr.start === 'number' ? expr.start : node.start
377
+ out.push({
378
+ line: offsetToLine(content, startOffset),
379
+ snippet: normalizeSnippet(content.slice(node.start, node.end))
380
+ })
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Знаходить підстановки IN (вираз) у TemplateLiteral, де вираз не пройшов числовий парсер.
474
386
  *
387
+ * Навіть у безпечному tagged template pool.request().query краще явно парсити значення (parseInt,
388
+ * Number, BigInt, parseFloat) та фільтрувати NaN. Див. також findUnsafeMssqlDynamicSqlListInText для
389
+ * випадків arr.join у списках.
475
390
  * @param {string} content вихідний код
476
- * @param {string} [virtualPath] шлях для вибору `lang`
391
+ * @param {string} [virtualPath] шлях для вибору мови парсера (lang)
477
392
  * @returns {{ line: number, snippet: string }[]} список порушень
478
393
  */
479
394
  export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan.ts') {
@@ -490,32 +405,7 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
490
405
 
491
406
  /** @type {{ line: number, snippet: string }[]} */
492
407
  const out = []
493
- walkAstWithAncestors(result.program, [], node => {
494
- if (node.type !== 'TemplateLiteral') return
495
- const quasis = node.quasis
496
- const expressions = node.expressions
497
- if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
498
-
499
- for (let i = 0; i < expressions.length; i++) {
500
- const q = quasis[i]
501
- const rawText =
502
- q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
503
- ? q.value.raw
504
- : ''
505
- if (!IN_PLACEHOLDER_END_RE.test(rawText)) continue
506
-
507
- const expr = expressions[i]
508
- if (!expr || typeof expr !== 'object') continue
509
- if (isJoinCall(expr)) continue
510
- if (isInListExpressionParsed(expr, declarators)) continue
511
-
512
- const startOffset = typeof expr.start === 'number' ? expr.start : node.start
513
- out.push({
514
- line: offsetToLine(content, startOffset),
515
- snippet: normalizeSnippet(content.slice(node.start, node.end))
516
- })
517
- }
518
- })
408
+ walkAstWithAncestors(result.program, [], node => collectInListUnparsedFromTemplate(node, content, declarators, out))
519
409
 
520
410
  return out
521
411
  }
@@ -528,4 +418,3 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
528
418
  export function isMssqlScanSourceFile(relativePathPosix) {
529
419
  return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
530
420
  }
531
-
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["unicorn", "oxc", "import", "jsdoc", "promise", "node"],
4
+ "jsPlugins": ["@e18e/eslint-plugin"],
5
+ "categories": {},
6
+ "rules": {},
7
+ "settings": {
8
+ "next": {
9
+ "rootDir": []
10
+ },
11
+ "jsdoc": {
12
+ "ignorePrivate": false,
13
+ "ignoreInternal": false,
14
+ "ignoreReplacesDocs": true,
15
+ "overrideReplacesDocs": true,
16
+ "augmentsExtendsReplacesDocs": false,
17
+ "implementsReplacesDocs": false,
18
+ "exemptDestructuredRootsFromChecks": false,
19
+ "tagNamePreference": {}
20
+ }
21
+ },
22
+ "env": {
23
+ "builtin": true
24
+ },
25
+ "globals": {},
26
+ "ignorePatterns": []
27
+ }