@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.
- package/bin/n-cursor.js +2 -1
- package/bin/rename-yaml-extensions.mjs +0 -2
- package/mdc/docker.mdc +2 -0
- package/mdc/js-lint.mdc +6 -4
- package/mdc/js-mssql.mdc +2 -0
- package/mdc/k8s.mdc +3 -3
- package/mdc/vue.mdc +3 -3
- package/package.json +3 -3
- package/scripts/auto-rules.mjs +46 -29
- package/scripts/check-capacitor.mjs +4 -4
- package/scripts/check-ga.mjs +27 -15
- package/scripts/check-js-bun-db.mjs +3 -3
- package/scripts/check-js-lint.mjs +114 -27
- package/scripts/check-js-mssql.mjs +155 -97
- package/scripts/check-k8s.mjs +204 -42
- package/scripts/check-nginx-default-tpl.mjs +1 -1
- package/scripts/check-php.mjs +0 -1
- package/scripts/check-text.mjs +1 -1
- package/scripts/check-vue.mjs +82 -45
- package/scripts/cli-entry.mjs +2 -5
- package/scripts/upgrade-nitra-cursor-and-install.mjs +1 -1
- package/scripts/utils/ast-scan-utils.mjs +154 -0
- package/scripts/utils/bun-sql-scan.mjs +10 -144
- package/scripts/utils/bunyan-imports.mjs +2 -36
- package/scripts/utils/mssql-pool-scan.mjs +76 -187
- package/scripts/utils/oxlint-canonical-skeleton.json +27 -0
- package/scripts/utils/oxlint-canonical.json +387 -0
- package/scripts/utils/oxlint-rules.tsv +359 -0
- package/scripts/utils/rebuild-oxlint-canonical.mjs +29 -0
- package/skills/lint/SKILL.md +1 -1
|
@@ -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
|
|
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
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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] шлях для вибору
|
|
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
|
+
}
|