@nitra/cursor 1.8.147 → 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 +1 -1
- package/bin/rename-yaml-extensions.mjs +0 -1
- package/mdc/docker.mdc +2 -0
- package/mdc/js-mssql.mdc +2 -0
- package/mdc/k8s.mdc +1 -1
- package/package.json +1 -1
- package/scripts/check-capacitor.mjs +4 -4
- package/scripts/check-js-lint.mjs +5 -1
- package/scripts/check-js-mssql.mjs +0 -1
- package/scripts/check-k8s.mjs +43 -10
- package/scripts/check-php.mjs +0 -1
- package/scripts/check-text.mjs +1 -1
- package/scripts/utils/ast-scan-utils.mjs +154 -0
- package/scripts/utils/bun-sql-scan.mjs +9 -143
- package/scripts/utils/bunyan-imports.mjs +2 -36
- package/scripts/utils/mssql-pool-scan.mjs +11 -135
- package/skills/lint/SKILL.md +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -561,7 +561,7 @@ function buildClaudeLintParallelismSectionLines() {
|
|
|
561
561
|
'## Лінт і ESLint (без паралельних запусків)',
|
|
562
562
|
'',
|
|
563
563
|
'Щоб не запускати **кілька** одночасних **`eslint`** (і не перевантажувати диск/CPU), **заборонено** стартувати `bun run lint` / `lint-js` / `eslint` **паралельно** в різних Bash-задачах, **фонових** shells чи **субагентах** (Task тощо). Має бути **один** послідовний прогон на сесію; команда **`/n-lint`** — **не** ділити на паралельні підзадачі. Деталі: `.cursor/skills/n-lint/SKILL.md`.',
|
|
564
|
-
''
|
|
564
|
+
''
|
|
565
565
|
]
|
|
566
566
|
}
|
|
567
567
|
|
package/mdc/docker.mdc
CHANGED
package/mdc/js-mssql.mdc
CHANGED
|
@@ -17,6 +17,7 @@ version: '1.2'
|
|
|
17
17
|
## Як виконувати запити (безпечно)
|
|
18
18
|
|
|
19
19
|
tagged template треба викликати на request-обʼєкті цього пулу:
|
|
20
|
+
|
|
20
21
|
```javascript
|
|
21
22
|
javascript// db.js
|
|
22
23
|
import sql from 'mssql';
|
|
@@ -58,6 +59,7 @@ const result = await pool.request().query`
|
|
|
58
59
|
### Не робити `query(\`...\`)`
|
|
59
60
|
|
|
60
61
|
javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
|
|
62
|
+
|
|
61
63
|
```javascript
|
|
62
64
|
await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
63
65
|
// ↑ круглі дужки замість бекті́ків = звичайна інтерполяція = SQL injection
|
package/mdc/k8s.mdc
CHANGED
|
@@ -314,7 +314,7 @@ data:
|
|
|
314
314
|
|
|
315
315
|
### Прод-оверрайди у `kustomization.yaml`
|
|
316
316
|
|
|
317
|
-
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути
|
|
317
|
+
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення **лише якщо** цей оверлей наслідує base-дерево, де є **Deployment** і **HPA/PDB** (тобто base реально дає dev-like HPA/PDB, які треба підняти в проді):
|
|
318
318
|
|
|
319
319
|
- для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
|
|
320
320
|
- для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
|
package/package.json
CHANGED
|
@@ -376,9 +376,7 @@ function extractNitraObjectBodySource(source) {
|
|
|
376
376
|
* @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
|
|
377
377
|
*/
|
|
378
378
|
function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
|
|
379
|
-
return (
|
|
380
|
-
RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
381
|
-
)
|
|
379
|
+
return RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
382
380
|
}
|
|
383
381
|
|
|
384
382
|
/**
|
|
@@ -443,7 +441,9 @@ export async function check() {
|
|
|
443
441
|
const { byPath, anyCapacitor } = acc
|
|
444
442
|
|
|
445
443
|
if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
|
|
446
|
-
pass(
|
|
444
|
+
pass(
|
|
445
|
+
'Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено'
|
|
446
|
+
)
|
|
447
447
|
return getExitCode()
|
|
448
448
|
}
|
|
449
449
|
|
|
@@ -18,7 +18,11 @@ import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-wo
|
|
|
18
18
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
19
19
|
|
|
20
20
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
|
-
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
21
|
+
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'utils',
|
|
24
|
+
'oxlint-canonical.json'
|
|
25
|
+
)
|
|
22
26
|
|
|
23
27
|
/** Очікуваний локальний скрипт. */
|
|
24
28
|
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -4803,26 +4803,59 @@ function checkProdOverridesInKustomization(kust, rel, fail, passFn) {
|
|
|
4803
4803
|
}
|
|
4804
4804
|
}
|
|
4805
4805
|
|
|
4806
|
+
/**
|
|
4807
|
+
* Чи прод-оверлей **реально потребує** overrides для HPA/PDB.
|
|
4808
|
+
*
|
|
4809
|
+
* Overrides потрібні лише якщо оверлей (non-dev-like) посилається на `…/k8s/…/base` і у **base**-дереві
|
|
4810
|
+
* одночасно є:
|
|
4811
|
+
* - `Deployment` (у шарі `…/k8s/…/base/`), і
|
|
4812
|
+
* - `HorizontalPodAutoscaler` і/або `PodDisruptionBudget`.
|
|
4813
|
+
*
|
|
4814
|
+
* Тоді base зазвичай тримає dev-like значення (`1`/`1`/`0`), і прод-оверлей має їх підняти (див. k8s.mdc).
|
|
4815
|
+
* @param {string} rootNorm нормалізований корінь репозиторію
|
|
4816
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
4817
|
+
* @returns {Promise<boolean>} true якщо потрібні overrides, інакше false
|
|
4818
|
+
*/
|
|
4819
|
+
export async function prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs) {
|
|
4820
|
+
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
4821
|
+
const segment = k8sEnvSegmentFromRelPath(rel)
|
|
4822
|
+
if (segment === null || isDevLikeK8sEnvSegment(segment)) return false
|
|
4823
|
+
|
|
4824
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
4825
|
+
if (kust === null) return false
|
|
4826
|
+
|
|
4827
|
+
const kustDir = dirname(kustAbs)
|
|
4828
|
+
const pathRefs = resourcePathRefsFromKustomizationObject(kust)
|
|
4829
|
+
const baseDirs = await k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm)
|
|
4830
|
+
if (baseDirs.length === 0) return false
|
|
4831
|
+
|
|
4832
|
+
const flags = await Promise.all(
|
|
4833
|
+
baseDirs.map(bd => kustomizeResourceTreeHpaPdbDeploymentFlags(join(bd, 'kustomization.yaml'), rootNorm))
|
|
4834
|
+
)
|
|
4835
|
+
|
|
4836
|
+
return flags.some(f => f.hasDeployment && (f.hasHpa || f.hasPdb))
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4806
4839
|
/**
|
|
4807
4840
|
* Для прод kustomization.yaml вимагає patches, що перевизначають **`/spec/minReplicas`** і **`/spec/maxReplicas`**
|
|
4808
|
-
* на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**.
|
|
4809
|
-
*
|
|
4841
|
+
* на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**.
|
|
4842
|
+
*
|
|
4843
|
+
* Не застосовується до dev-like (base / dev / *-qa).
|
|
4844
|
+
*
|
|
4845
|
+
* Також **не застосовується**, якщо оверлей не наслідує base з Deployment + HPA/PDB (див. `prodOverlayNeedsHpaPdbOverrides`).
|
|
4810
4846
|
* @param {string} root корінь репозиторію
|
|
4811
4847
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
4812
4848
|
* @param {(msg: string) => void} fail callback при помилці
|
|
4813
4849
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
4814
4850
|
*/
|
|
4815
4851
|
async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, passFn) {
|
|
4852
|
+
const rootNorm = resolve(root)
|
|
4816
4853
|
const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
|
|
4817
4854
|
for (const kustAbs of kustFiles) {
|
|
4818
|
-
const rel = relative(
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
if (kust !== null) {
|
|
4823
|
-
checkProdOverridesInKustomization(kust, rel, fail, passFn)
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4855
|
+
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
4856
|
+
if (!(await prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs))) continue
|
|
4857
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
4858
|
+
if (kust !== null) checkProdOverridesInKustomization(kust, rel, fail, passFn)
|
|
4826
4859
|
}
|
|
4827
4860
|
}
|
|
4828
4861
|
|
package/scripts/check-php.mjs
CHANGED
package/scripts/check-text.mjs
CHANGED
|
@@ -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 {
|
|
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 вихідний код
|
|
@@ -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 фрагмент коду
|
|
@@ -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 вихідний код
|
|
@@ -449,13 +332,7 @@ function isInListExpressionParsed(expr, declarators, seen = new Set()) {
|
|
|
449
332
|
const inits = declarators
|
|
450
333
|
.filter(d => {
|
|
451
334
|
const id = d.id
|
|
452
|
-
return
|
|
453
|
-
!!id &&
|
|
454
|
-
typeof id === 'object' &&
|
|
455
|
-
id.type === 'Identifier' &&
|
|
456
|
-
id.name === expr.name &&
|
|
457
|
-
!!d.init
|
|
458
|
-
)
|
|
335
|
+
return !!id && typeof id === 'object' && id.type === 'Identifier' && id.name === expr.name && !!d.init
|
|
459
336
|
})
|
|
460
337
|
.map(d => d.init)
|
|
461
338
|
if (inits.length === 0) return false
|
|
@@ -541,4 +418,3 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
|
|
|
541
418
|
export function isMssqlScanSourceFile(relativePathPosix) {
|
|
542
419
|
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
543
420
|
}
|
|
544
|
-
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -53,7 +53,7 @@ bun run lint
|
|
|
53
53
|
|
|
54
54
|
**Чому взагалі кілька `eslint`:** оркестратор (Claude Code, Cursor тощо) може **розпаралелити** роботу: кілька **субагентів** / **паралельних Bash-задач** / **фонових shell**, і кожен **сам** виконує **`/n-lint`** або запускає **`eslint`**. Тоді навантаження **множиться**: не один прогон лінту, а **N прогонів** одночасно.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
### Що робити агенту під час виконання цього скілу (обов’язково)
|
|
57
57
|
|
|
58
58
|
1. **Один** запуск **`bun run lint`** (або всі кроки **`lint`**, як у **`package.json`**) — у **одному** foreground shell, **без** `run_in_background` / фонових копій тієї ж команди.
|
|
59
59
|
2. **Не** викликати **паралельні субагенти** (subagent, Task, «розбий на N паралельних завдань») лише заради лінту в одному репозиторії. Лінт не потребує шардінгу: один процес, послідовно.
|