@nitra/cursor 1.8.155 → 1.8.157
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/CHANGELOG.md +18 -0
- package/bin/auto-rules.md +2 -0
- package/mdc/hasura.mdc +31 -0
- package/mdc/js-lint.mdc +29 -4
- package/mdc/js-mssql.mdc +1 -1
- package/mdc/js-run.mdc +25 -13
- package/mdc/npm-module.mdc +9 -1
- package/package.json +3 -2
- package/scripts/auto-rules.mjs +58 -27
- package/scripts/build-agents-commands.mjs +7 -7
- package/scripts/check-hasura.mjs +219 -0
- package/scripts/check-js-bun-db.mjs +64 -45
- package/scripts/check-js-lint.mjs +10 -8
- package/scripts/check-js-run.mjs +62 -36
- package/scripts/check-k8s.mjs +455 -197
- package/scripts/check-npm-module.mjs +55 -0
- package/scripts/utils/bun-sql-scan.mjs +5 -2
- package/scripts/utils/check-env-scan.mjs +222 -64
- package/scripts/utils/conn-imports-scan.mjs +13 -3
- package/scripts/utils/mssql-pool-scan.mjs +57 -38
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* Якщо таких файлів немає — layout через `npm/tsconfig.emit-types.json`: поле `types` має вказувати на існуючий
|
|
10
10
|
* файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
|
|
11
11
|
*
|
|
12
|
+
* Окремо перевіряється `npm/CHANGELOG.md`: файл існує, є в `files` у `npm/package.json` і містить запис
|
|
13
|
+
* для поточної версії (формат `## [X.Y.Z] - YYYY-MM-DD`, Keep a Changelog).
|
|
14
|
+
*
|
|
12
15
|
* Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
|
|
13
16
|
*/
|
|
14
17
|
import { existsSync } from 'node:fs'
|
|
@@ -33,6 +36,9 @@ const TYPES_INDEX = './types/index.d.ts'
|
|
|
33
36
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
34
37
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
35
38
|
|
|
39
|
+
/** Шлях до `CHANGELOG.md` в каталозі npm-модуля */
|
|
40
|
+
const CHANGELOG_PATH = 'npm/CHANGELOG.md'
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
38
44
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
@@ -194,6 +200,53 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
194
200
|
}
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Чи містить текст `CHANGELOG.md` запис заголовка для конкретної версії
|
|
205
|
+
* у форматі Keep a Changelog: `## [X.Y.Z]` (з опційним `- YYYY-MM-DD`).
|
|
206
|
+
* @param {string} text вміст `CHANGELOG.md`
|
|
207
|
+
* @param {string} version номер версії з `npm/package.json`
|
|
208
|
+
* @returns {boolean} `true`, якщо знайдено заголовок версії
|
|
209
|
+
*/
|
|
210
|
+
function changelogHasVersionEntry(text, version) {
|
|
211
|
+
const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
|
|
212
|
+
const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
|
|
213
|
+
return re.test(text)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Перевіряє наявність і вміст `npm/CHANGELOG.md`, а також що він є в `files` у `npm/package.json`.
|
|
218
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
219
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
220
|
+
*/
|
|
221
|
+
async function checkChangelog(passFn, failFn) {
|
|
222
|
+
if (!existsSync(CHANGELOG_PATH)) {
|
|
223
|
+
failFn(`Відсутній ${CHANGELOG_PATH} (npm-module.mdc: CHANGELOG)`)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
passFn(`${CHANGELOG_PATH} існує`)
|
|
227
|
+
|
|
228
|
+
if (existsSync('npm/package.json')) {
|
|
229
|
+
const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
230
|
+
if (Array.isArray(npmPkg.files) && npmPkg.files.includes('CHANGELOG.md')) {
|
|
231
|
+
passFn('npm/package.json: files містить "CHANGELOG.md"')
|
|
232
|
+
} else {
|
|
233
|
+
failFn('npm/package.json: масив files має містити "CHANGELOG.md"')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const version = typeof npmPkg.version === 'string' ? npmPkg.version : null
|
|
237
|
+
if (version) {
|
|
238
|
+
const text = await readFile(CHANGELOG_PATH, 'utf8')
|
|
239
|
+
if (changelogHasVersionEntry(text, version)) {
|
|
240
|
+
passFn(`${CHANGELOG_PATH}: знайдено запис для версії ${version}`)
|
|
241
|
+
} else {
|
|
242
|
+
failFn(
|
|
243
|
+
`${CHANGELOG_PATH}: відсутній запис для поточної версії ${version} (формат Keep a Changelog: "## [${version}] - YYYY-MM-DD")`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
197
250
|
/**
|
|
198
251
|
* Перевіряє npm/tsconfig.emit-types.json.
|
|
199
252
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
@@ -338,6 +391,8 @@ export async function check() {
|
|
|
338
391
|
|
|
339
392
|
await checkNpmPackageJson(useSrcJsLayout, pass, fail)
|
|
340
393
|
|
|
394
|
+
await checkChangelog(pass, fail)
|
|
395
|
+
|
|
341
396
|
if (!useSrcJsLayout) {
|
|
342
397
|
await checkEmitTypesConfig(pass, fail)
|
|
343
398
|
}
|
|
@@ -116,7 +116,8 @@ function isEmptyListTest(test, name) {
|
|
|
116
116
|
if (!['===', '==', '<=', '<'].includes(operator)) return false
|
|
117
117
|
if (isLengthMember(left, name) && isZeroNumberLiteral(right)) return true
|
|
118
118
|
// допускаємо `0 === ids.length` теж
|
|
119
|
-
if (isZeroNumberLiteral(left) && isLengthMember(right, name) && (operator === '===' || operator === '=='))
|
|
119
|
+
if (isZeroNumberLiteral(left) && isLengthMember(right, name) && (operator === '===' || operator === '=='))
|
|
120
|
+
return true
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
return false
|
|
@@ -307,7 +308,9 @@ function collectInListGuardViolationsFromTemplate(template, ancestors, content,
|
|
|
307
308
|
for (const [i, expr] of expressions.entries()) {
|
|
308
309
|
const q = quasis[i]
|
|
309
310
|
const raw =
|
|
310
|
-
q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
|
|
311
|
+
q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
|
|
312
|
+
? q.value.raw
|
|
313
|
+
: ''
|
|
311
314
|
if (!IN_PLACEHOLDER_END_RE.test(raw)) continue
|
|
312
315
|
|
|
313
316
|
const extracted = extractInListVarNameFromExpr(expr)
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AST-сканер
|
|
2
|
+
* AST-сканер правила «process.env / CheckEnv» (js-run.mdc).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Правило в .mdc формулює два контракти:
|
|
5
|
+
* 1. Прямий доступ до `process.env.X` має бути замінено на `env` — з пакета
|
|
6
|
+
* `@nitra/check-env` (для обов'язкових змінних, із викликом `checkEnv([...])`)
|
|
7
|
+
* або з `node:process` (для опційних). Тому будь-яке `process.env.X` сканер
|
|
8
|
+
* завжди реєструє як порушення з порадою про конкретну заміну.
|
|
9
|
+
* 2. Якщо у файл імпортовано `env` саме з `@nitra/check-env`, то кожне `env.X`
|
|
10
|
+
* має бути закрите літеральним викликом `checkEnv(['X', ...])` у тому ж файлі
|
|
11
|
+
* (порядок не важливий, кілька викликів зливаються в один список).
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* Обидва контракти можна точково «приглушити» коментарем-маркером
|
|
14
|
+
* `// \@nitra/cursor ignore-next-line checkEnv` на рядку безпосередньо перед
|
|
15
|
+
* порушенням — це залишається сумісним escape-hatch для legacy-коду.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - `const { X: alias } = process.env` (ім'я з ключа, не з alias).
|
|
17
|
+
* Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
|
|
18
|
+
* файлу не використовується, лише сирий текст рядка з коментарем перевіряється
|
|
19
|
+
* на маркер. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
20
|
+
* треба полагодити синтаксис.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
22
|
+
* Покриті форми доступу:
|
|
23
|
+
* - `process.env.X` / `process.env['X']` (як MemberExpression);
|
|
24
|
+
* - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа, не з alias);
|
|
25
|
+
* - аналогічно для `env.X` / `env['X']` / `const { X } = env`,
|
|
26
|
+
* де `env` має бути імпортований з `@nitra/check-env` (інакше ігноруємо —
|
|
27
|
+
* це може бути локальна змінна чи `env` з `node:process`).
|
|
28
|
+
*
|
|
29
|
+
* Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
|
|
21
30
|
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
22
31
|
*/
|
|
23
32
|
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-scan-utils.mjs'
|
|
@@ -25,6 +34,8 @@ import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-sc
|
|
|
25
34
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
26
35
|
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
27
36
|
|
|
37
|
+
const CHECK_ENV_PACKAGE = '@nitra/check-env'
|
|
38
|
+
|
|
28
39
|
/**
|
|
29
40
|
* Чи є цей вузол виразом `process.env`.
|
|
30
41
|
* @param {unknown} node AST вузол
|
|
@@ -46,8 +57,8 @@ function isProcessEnvAccess(node) {
|
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
/**
|
|
49
|
-
* Витягує ім'я ENV з MemberExpression
|
|
50
|
-
* @param {Record<string, unknown>} node MemberExpression, чий object — `process.env`
|
|
60
|
+
* Витягує ім'я ENV з MemberExpression `obj.X` або `obj['X']`.
|
|
61
|
+
* @param {Record<string, unknown>} node MemberExpression, чий object — `process.env` або `env`
|
|
51
62
|
* @returns {string | null} ім'я змінної оточення або null, якщо ключ не статичний
|
|
52
63
|
*/
|
|
53
64
|
function envNameFromMember(node) {
|
|
@@ -88,9 +99,39 @@ function collectCheckedEnvNames(programNode) {
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
/**
|
|
91
|
-
* Чи
|
|
102
|
+
* Чи імпортовано локальний ідентифікатор `env` саме з `@nitra/check-env`.
|
|
103
|
+
* Перевіряє ImportDeclaration на specifier {imported.name === 'env', local.name === 'env'}.
|
|
104
|
+
* Aliased-варіанти (`{ env as x }`) свідомо не підтримуються — у наших правилах
|
|
105
|
+
* приклади завжди використовують канонічне ім'я `env`.
|
|
106
|
+
* @param {unknown} programNode корінь AST
|
|
107
|
+
* @returns {boolean} true, якщо у файлі є `import { env } from '@nitra/check-env'`
|
|
108
|
+
*/
|
|
109
|
+
function hasCheckEnvImport(programNode) {
|
|
110
|
+
let found = false
|
|
111
|
+
walkAstWithAncestors(programNode, [], node => {
|
|
112
|
+
if (found) return
|
|
113
|
+
if (node.type !== 'ImportDeclaration') return
|
|
114
|
+
const source = node.source
|
|
115
|
+
if (!source || typeof source !== 'object' || source.value !== CHECK_ENV_PACKAGE) return
|
|
116
|
+
const specifiers = node.specifiers
|
|
117
|
+
if (!Array.isArray(specifiers)) return
|
|
118
|
+
for (const s of specifiers) {
|
|
119
|
+
if (!s || typeof s !== 'object' || s.type !== 'ImportSpecifier') continue
|
|
120
|
+
const imported = s.imported
|
|
121
|
+
const local = s.local
|
|
122
|
+
if (!imported || imported.name !== 'env') continue
|
|
123
|
+
if (!local || local.name !== 'env') continue
|
|
124
|
+
found = true
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
return found
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Чи закритий рядок ignore-коментарем `// \@nitra/cursor ignore-next-line checkEnv`.
|
|
92
133
|
* @param {string[]} lines рядки файлу (split за \n, без CR)
|
|
93
|
-
* @param {number} oneBasedLine 1-based номер рядка з
|
|
134
|
+
* @param {number} oneBasedLine 1-based номер рядка з порушенням
|
|
94
135
|
* @returns {boolean} true, якщо попередній рядок містить маркер
|
|
95
136
|
*/
|
|
96
137
|
function hasIgnoreDirective(lines, oneBasedLine) {
|
|
@@ -100,75 +141,192 @@ function hasIgnoreDirective(lines, oneBasedLine) {
|
|
|
100
141
|
}
|
|
101
142
|
|
|
102
143
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @param {
|
|
107
|
-
* @
|
|
108
|
-
* @returns {{ line: number, name: string }[]} список порушень
|
|
144
|
+
* Чи є вузол MemberExpression виду `env.X` / `env['X']`, де `env` — Identifier
|
|
145
|
+
* (в AST oxc-parser globals і локальні імпорти не розрізняються — фільтр джерела
|
|
146
|
+
* робиться на рівні `hasCheckEnvImport`).
|
|
147
|
+
* @param {unknown} node AST вузол
|
|
148
|
+
* @returns {boolean} true, якщо це `env.<...>`
|
|
109
149
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
function isEnvIdentifierMember(node) {
|
|
151
|
+
if (!node || typeof node !== 'object' || node.type !== 'MemberExpression') return false
|
|
152
|
+
const obj = node.object
|
|
153
|
+
return !!obj && obj.type === 'Identifier' && obj.name === 'env'
|
|
154
|
+
}
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {{
|
|
158
|
+
* line: number,
|
|
159
|
+
* name: string,
|
|
160
|
+
* kind: 'process-env' | 'check-env-missing-checkEnv'
|
|
161
|
+
* }} EnvViolation
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Чи parent — це MemberExpression, у якому `node` (process.env) є об'єктом доступу.
|
|
166
|
+
* @param {unknown} parent ancestor вузла
|
|
167
|
+
* @param {unknown} node перевіряємий вузол `process.env`
|
|
168
|
+
* @returns {boolean} true для `process.env.X` / `process.env['X']`
|
|
169
|
+
*/
|
|
170
|
+
function isParentEnvMember(parent, node) {
|
|
171
|
+
return !!parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Чи parent — це VariableDeclarator виду `const { ... } = <node>`.
|
|
176
|
+
* @param {unknown} parent ancestor вузла
|
|
177
|
+
* @param {unknown} node перевіряємий вузол (process.env або Identifier `env`)
|
|
178
|
+
* @returns {boolean} true для `const { ... } = node`
|
|
179
|
+
*/
|
|
180
|
+
function isParentObjectPatternDeclarator(parent, node) {
|
|
181
|
+
return (
|
|
182
|
+
!!parent &&
|
|
183
|
+
typeof parent === 'object' &&
|
|
184
|
+
parent.type === 'VariableDeclarator' &&
|
|
185
|
+
parent.init === node &&
|
|
186
|
+
!!parent.id &&
|
|
187
|
+
parent.id.type === 'ObjectPattern' &&
|
|
188
|
+
Array.isArray(parent.id.properties)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Чи node — це VariableDeclarator виду `const { ... } = env`, де `env` — Identifier.
|
|
194
|
+
* @param {Record<string, unknown>} node AST-вузол
|
|
195
|
+
* @returns {boolean} true для `const { ... } = env`
|
|
196
|
+
*/
|
|
197
|
+
function isEnvObjectPatternDeclarator(node) {
|
|
198
|
+
return (
|
|
199
|
+
node.type === 'VariableDeclarator' &&
|
|
200
|
+
!!node.init &&
|
|
201
|
+
node.init.type === 'Identifier' &&
|
|
202
|
+
node.init.name === 'env' &&
|
|
203
|
+
!!node.id &&
|
|
204
|
+
node.id.type === 'ObjectPattern' &&
|
|
205
|
+
Array.isArray(node.id.properties)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
116
208
|
|
|
117
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Перебирає AST і для кожного знайденого доступу до `process.env` чи `env`
|
|
211
|
+
* (де `env` — імпорт з `@nitra/check-env`) реєструє порушення відповідного типу.
|
|
212
|
+
* @param {unknown} program корінь AST
|
|
213
|
+
* @param {string} content вихідний код (для offset → line)
|
|
214
|
+
* @param {string[]} lines split-рядки content (для ignore-маркера)
|
|
215
|
+
* @param {Set<string>} checkedNames імена, закриті літеральним `checkEnv([...])`
|
|
216
|
+
* @param {boolean} envFromCheckEnv чи імпортовано `env` саме з `@nitra/check-env`
|
|
217
|
+
* @returns {EnvViolation[]} список порушень (відсортований за порядком зустрічі в AST)
|
|
218
|
+
*/
|
|
219
|
+
function collectViolations(program, content, lines, checkedNames, envFromCheckEnv) {
|
|
220
|
+
/** @type {EnvViolation[]} */
|
|
118
221
|
const out = []
|
|
119
222
|
/** @type {Set<string>} */
|
|
120
223
|
const reported = new Set()
|
|
121
224
|
|
|
122
225
|
/**
|
|
123
|
-
* Реєструє порушення з дедуплікацією за «name
|
|
226
|
+
* Реєструє порушення з дедуплікацією за «kind|name|line» і урахуванням ignore-маркера.
|
|
227
|
+
* @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
|
|
124
228
|
* @param {string} name ім'я ENV
|
|
125
229
|
* @param {number} line 1-based рядок
|
|
126
230
|
*/
|
|
127
|
-
function report(name, line) {
|
|
128
|
-
if (checked.has(name)) return
|
|
231
|
+
function report(kind, name, line) {
|
|
129
232
|
if (hasIgnoreDirective(lines, line)) return
|
|
130
|
-
const key = `${name}
|
|
233
|
+
const key = `${kind}|${name}|${line}`
|
|
131
234
|
if (reported.has(key)) return
|
|
132
235
|
reported.add(key)
|
|
133
|
-
out.push({ name, line })
|
|
236
|
+
out.push({ kind, name, line })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Реєструє порушення для всіх статичних ключів ObjectPattern (`const { A, B } = …`).
|
|
241
|
+
* @param {Record<string, unknown>} declarator VariableDeclarator з ObjectPattern у `id`
|
|
242
|
+
* @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
|
|
243
|
+
* @param {(name: string) => boolean} skipName предикат «пропустити це ім'я» (наприклад, вже у checkEnv)
|
|
244
|
+
*/
|
|
245
|
+
function reportObjectPatternKeys(declarator, kind, skipName) {
|
|
246
|
+
const fallbackOffset = declarator.start
|
|
247
|
+
for (const p of declarator.id.properties) {
|
|
248
|
+
const name = staticPropertyName(p)
|
|
249
|
+
if (!name || skipName(name)) continue
|
|
250
|
+
report(kind, name, offsetToLine(content, p.start ?? fallbackOffset))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Обробка `process.env`-доступу: і `parent.X`, і деструктуризація.
|
|
256
|
+
* @param {unknown} node AST-вузол `process.env`
|
|
257
|
+
* @param {unknown[]} ancestors стек предків з walkAstWithAncestors
|
|
258
|
+
*/
|
|
259
|
+
function handleProcessEnv(node, ancestors) {
|
|
260
|
+
const parent = ancestors.at(-1)
|
|
261
|
+
if (isParentEnvMember(parent, node)) {
|
|
262
|
+
const envName = envNameFromMember(parent)
|
|
263
|
+
if (envName) report('process-env', envName, offsetToLine(content, parent.start))
|
|
264
|
+
}
|
|
265
|
+
if (isParentObjectPatternDeclarator(parent, node)) {
|
|
266
|
+
reportObjectPatternKeys(parent, 'process-env', () => false)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Обробка вузлів, що стосуються `env` з `@nitra/check-env`.
|
|
272
|
+
* @param {Record<string, unknown>} node AST-вузол
|
|
273
|
+
*/
|
|
274
|
+
function handleCheckEnvAccess(node) {
|
|
275
|
+
if (isEnvIdentifierMember(node)) {
|
|
276
|
+
const envName = envNameFromMember(node)
|
|
277
|
+
if (envName && !checkedNames.has(envName)) {
|
|
278
|
+
report('check-env-missing-checkEnv', envName, offsetToLine(content, node.start))
|
|
279
|
+
}
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
if (isEnvObjectPatternDeclarator(node)) {
|
|
283
|
+
reportObjectPatternKeys(node, 'check-env-missing-checkEnv', name => checkedNames.has(name))
|
|
284
|
+
}
|
|
134
285
|
}
|
|
135
286
|
|
|
136
287
|
walkAstWithAncestors(program, [], (node, ancestors) => {
|
|
137
288
|
if (isProcessEnvAccess(node)) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node) {
|
|
141
|
-
const envName = envNameFromMember(parent)
|
|
142
|
-
if (envName) report(envName, offsetToLine(content, parent.start))
|
|
143
|
-
}
|
|
144
|
-
// const { X, Y } = process.env → беремо імена з ObjectPattern
|
|
145
|
-
if (
|
|
146
|
-
parent &&
|
|
147
|
-
typeof parent === 'object' &&
|
|
148
|
-
parent.type === 'VariableDeclarator' &&
|
|
149
|
-
parent.init === node &&
|
|
150
|
-
parent.id &&
|
|
151
|
-
parent.id.type === 'ObjectPattern' &&
|
|
152
|
-
Array.isArray(parent.id.properties)
|
|
153
|
-
) {
|
|
154
|
-
for (const p of parent.id.properties) {
|
|
155
|
-
if (!p || typeof p !== 'object' || p.type !== 'Property') continue
|
|
156
|
-
if (p.computed) continue
|
|
157
|
-
const key = p.key
|
|
158
|
-
if (!key || typeof key !== 'object') continue
|
|
159
|
-
/** @type {string | null} */
|
|
160
|
-
let name = null
|
|
161
|
-
if (key.type === 'Identifier' && typeof key.name === 'string') name = key.name
|
|
162
|
-
else if (key.type === 'Literal' && typeof key.value === 'string') name = key.value
|
|
163
|
-
if (name) report(name, offsetToLine(content, p.start ?? parent.start))
|
|
164
|
-
}
|
|
165
|
-
}
|
|
289
|
+
handleProcessEnv(node, ancestors)
|
|
290
|
+
return
|
|
166
291
|
}
|
|
292
|
+
if (envFromCheckEnv) handleCheckEnvAccess(node)
|
|
167
293
|
})
|
|
168
294
|
|
|
169
295
|
return out
|
|
170
296
|
}
|
|
171
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Витягує статичне ім'я з вузла Property у ObjectPattern.
|
|
300
|
+
* @param {unknown} property AST-вузол ObjectPattern.properties[i]
|
|
301
|
+
* @returns {string | null} ім'я ключа або null
|
|
302
|
+
*/
|
|
303
|
+
function staticPropertyName(property) {
|
|
304
|
+
if (!property || typeof property !== 'object' || property.type !== 'Property') return null
|
|
305
|
+
if (property.computed) return null
|
|
306
|
+
const key = property.key
|
|
307
|
+
if (!key || typeof key !== 'object') return null
|
|
308
|
+
if (key.type === 'Identifier' && typeof key.name === 'string') return key.name
|
|
309
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Знаходить порушення правила «process.env / CheckEnv» у файлі.
|
|
315
|
+
* @param {string} content вихідний код
|
|
316
|
+
* @param {string} [virtualPath] шлях для вибору `lang` парсера
|
|
317
|
+
* @returns {EnvViolation[]} список порушень із типом, іменем змінної та рядком
|
|
318
|
+
*/
|
|
319
|
+
export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
|
|
320
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
321
|
+
if (!program) return []
|
|
322
|
+
|
|
323
|
+
const checked = collectCheckedEnvNames(program)
|
|
324
|
+
const envFromCheckEnv = hasCheckEnvImport(program)
|
|
325
|
+
const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
|
|
326
|
+
|
|
327
|
+
return collectViolations(program, content, lines, checked, envFromCheckEnv)
|
|
328
|
+
}
|
|
329
|
+
|
|
172
330
|
/**
|
|
173
331
|
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
|
|
174
332
|
* @param {string} relativePathPosix відносний шлях (posix)
|
|
@@ -20,7 +20,17 @@ import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.m
|
|
|
20
20
|
import { parseSync } from 'oxc-parser'
|
|
21
21
|
|
|
22
22
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Прибирає хвостові `/` зі шляху без використання regex (щоб не тригерити slow-regex попередження).
|
|
26
|
+
* @param {string} s рядок зі шляхом
|
|
27
|
+
* @returns {string} `s` без хвостових `/`
|
|
28
|
+
*/
|
|
29
|
+
function stripTrailingSlashes(s) {
|
|
30
|
+
let end = s.length
|
|
31
|
+
while (end > 0 && s.codePointAt(end - 1) === 47) end--
|
|
32
|
+
return end === s.length ? s : s.slice(0, end)
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
36
|
* Нормалізує шлях до posix без хвостових слешів.
|
|
@@ -30,7 +40,7 @@ const TRAILING_SLASH_RE = /\/+$/u
|
|
|
30
40
|
function toPosixDir(p) {
|
|
31
41
|
let s = String(p).replaceAll('\\', '/').trim()
|
|
32
42
|
if (s.startsWith('./')) s = s.slice(2)
|
|
33
|
-
return s
|
|
43
|
+
return stripTrailingSlashes(s)
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
/**
|
|
@@ -57,7 +67,7 @@ export function resolveConnDirFromPackageJson(pkgJson) {
|
|
|
57
67
|
// Прибираємо хвіст `*`, потім слеші
|
|
58
68
|
let s = toPosixDir(raw)
|
|
59
69
|
if (s.endsWith('/*')) s = s.slice(0, -2)
|
|
60
|
-
return s
|
|
70
|
+
return stripTrailingSlashes(s) || fallback
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
/**
|
|
@@ -36,60 +36,81 @@ const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
|
36
36
|
const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
|
|
37
37
|
const NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Чи є вузол літералом числа `0` (як NumericLiteral, так і generic Literal).
|
|
41
|
+
* @param {unknown} node AST-вузол
|
|
42
|
+
* @returns {boolean} true для `0`
|
|
43
|
+
*/
|
|
44
|
+
function isZeroLiteral(node) {
|
|
45
|
+
return (
|
|
46
|
+
!!node &&
|
|
47
|
+
typeof node === 'object' &&
|
|
48
|
+
((node.type === 'NumericLiteral' && node.value === 0) || (node.type === 'Literal' && node.value === 0))
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Чи `node` — це MemberExpression `name.length` (некомп'ютерований Identifier.Identifier).
|
|
54
|
+
* @param {unknown} node AST-вузол
|
|
55
|
+
* @param {string} name очікуване імʼя змінної-власника
|
|
56
|
+
* @returns {boolean} true для `<name>.length`
|
|
57
|
+
*/
|
|
58
|
+
function isLengthMemberOf(node, name) {
|
|
59
|
+
return (
|
|
60
|
+
!!node &&
|
|
61
|
+
typeof node === 'object' &&
|
|
62
|
+
node.type === 'MemberExpression' &&
|
|
63
|
+
!node.computed &&
|
|
64
|
+
!!node.object &&
|
|
65
|
+
node.object.type === 'Identifier' &&
|
|
66
|
+
node.object.name === name &&
|
|
67
|
+
!!node.property &&
|
|
68
|
+
node.property.type === 'Identifier' &&
|
|
69
|
+
node.property.name === 'length'
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const EMPTY_LIST_BINARY_OPERATORS = new Set(['===', '==', '<=', '<'])
|
|
74
|
+
const EMPTY_LIST_REVERSED_OPERATORS = new Set(['===', '=='])
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Чи `BinaryExpression`-тест з оператором з {@link EMPTY_LIST_BINARY_OPERATORS}
|
|
78
|
+
* порівнює `<name>.length` з літералом `0` (у будь-якому порядку для `===`/`==`).
|
|
79
|
+
* @param {Record<string, unknown>} test BinaryExpression вузол
|
|
80
|
+
* @param {string} name імʼя змінної списку
|
|
81
|
+
* @returns {boolean} true для умов виду `name.length === 0`, `name.length <= 0`, `0 === name.length` тощо
|
|
82
|
+
*/
|
|
83
|
+
function isEmptyListBinaryTest(test, name) {
|
|
84
|
+
const { left, right, operator } = test
|
|
85
|
+
if (!EMPTY_LIST_BINARY_OPERATORS.has(operator)) return false
|
|
86
|
+
if (isLengthMemberOf(left, name) && isZeroLiteral(right)) return true
|
|
87
|
+
return EMPTY_LIST_REVERSED_OPERATORS.has(operator) && isZeroLiteral(left) && isLengthMemberOf(right, name)
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
/**
|
|
40
91
|
* Чи містить тест if-умови перевірку “список порожній”.
|
|
41
92
|
* Підтримує базові форми:
|
|
42
93
|
* - `if (!ids.length) ...`
|
|
43
94
|
* - `if (ids.length === 0) ...` / `<= 0` / `< 1`
|
|
44
|
-
*
|
|
45
95
|
* @param {unknown} test IfStatement.test
|
|
46
96
|
* @param {string} name імʼя змінної списку
|
|
47
|
-
* @returns {boolean}
|
|
97
|
+
* @returns {boolean} true, якщо тест перевіряє, що `name` — порожній масив
|
|
48
98
|
*/
|
|
49
99
|
function isEmptyListTest(test, name) {
|
|
50
100
|
if (!test || typeof test !== 'object') return false
|
|
51
|
-
|
|
52
101
|
if (test.type === 'UnaryExpression' && test.operator === '!') {
|
|
53
|
-
|
|
54
|
-
if (!arg || typeof arg !== 'object') return false
|
|
55
|
-
if (arg.type === 'MemberExpression' && !arg.computed) {
|
|
56
|
-
const obj = arg.object
|
|
57
|
-
const prop = arg.property
|
|
58
|
-
return !!obj && obj.type === 'Identifier' && obj.name === name && !!prop && prop.type === 'Identifier' && prop.name === 'length'
|
|
59
|
-
}
|
|
102
|
+
return isLengthMemberOf(test.argument, name)
|
|
60
103
|
}
|
|
61
|
-
|
|
62
104
|
if (test.type === 'BinaryExpression') {
|
|
63
|
-
|
|
64
|
-
const isLen = node =>
|
|
65
|
-
!!node &&
|
|
66
|
-
typeof node === 'object' &&
|
|
67
|
-
node.type === 'MemberExpression' &&
|
|
68
|
-
!node.computed &&
|
|
69
|
-
node.object &&
|
|
70
|
-
node.object.type === 'Identifier' &&
|
|
71
|
-
node.object.name === name &&
|
|
72
|
-
node.property &&
|
|
73
|
-
node.property.type === 'Identifier' &&
|
|
74
|
-
node.property.name === 'length'
|
|
75
|
-
const isZero = node =>
|
|
76
|
-
!!node &&
|
|
77
|
-
typeof node === 'object' &&
|
|
78
|
-
((node.type === 'NumericLiteral' && node.value === 0) || (node.type === 'Literal' && node.value === 0))
|
|
79
|
-
|
|
80
|
-
if (!['===', '==', '<=', '<'].includes(operator)) return false
|
|
81
|
-
if (isLen(left) && isZero(right)) return true
|
|
82
|
-
// допускаємо `0 === ids.length` теж
|
|
83
|
-
if (isZero(left) && isLen(right) && (operator === '===' || operator === '==')) return true
|
|
105
|
+
return isEmptyListBinaryTest(test, name)
|
|
84
106
|
}
|
|
85
|
-
|
|
86
107
|
return false
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
/**
|
|
90
111
|
* Чи є в consequent (або в його BlockStatement) ThrowStatement.
|
|
91
112
|
* @param {unknown} consequent IfStatement.consequent
|
|
92
|
-
* @returns {boolean}
|
|
113
|
+
* @returns {boolean} true, якщо гілка consequent містить `throw`
|
|
93
114
|
*/
|
|
94
115
|
function consequentHasThrow(consequent) {
|
|
95
116
|
if (!consequent || typeof consequent !== 'object') return false
|
|
@@ -105,7 +126,7 @@ function consequentHasThrow(consequent) {
|
|
|
105
126
|
* @param {unknown} block BlockStatement
|
|
106
127
|
* @param {number} statementIndex індекс statement, перед яким шукаємо guard
|
|
107
128
|
* @param {string} name імʼя змінної списку
|
|
108
|
-
* @returns {boolean}
|
|
129
|
+
* @returns {boolean} true, якщо знайдено guard `if (empty(name)) throw` до statementIndex
|
|
109
130
|
*/
|
|
110
131
|
function hasEmptyGuardBefore(block, statementIndex, name) {
|
|
111
132
|
if (!block || typeof block !== 'object' || block.type !== 'BlockStatement') return false
|
|
@@ -125,7 +146,7 @@ function hasEmptyGuardBefore(block, statementIndex, name) {
|
|
|
125
146
|
/**
|
|
126
147
|
* Знаходить найближчий enclosing BlockStatement і statement всередині нього.
|
|
127
148
|
* @param {unknown[]} ancestors ancestors масив з walkAstWithAncestors
|
|
128
|
-
* @returns {{ block: unknown, index: number } | null}
|
|
149
|
+
* @returns {{ block: unknown, index: number } | null} пара (BlockStatement, індекс statement) або null
|
|
129
150
|
*/
|
|
130
151
|
function findEnclosingBlockAndStatementIndex(ancestors) {
|
|
131
152
|
if (!Array.isArray(ancestors) || ancestors.length === 0) return null
|
|
@@ -492,7 +513,6 @@ function collectInListUnparsedFromTemplate(node, content, declarators, out) {
|
|
|
492
513
|
* Збирає порушення для одного TemplateLiteral: якщо у `IN (${...})`:
|
|
493
514
|
* - `${...}` не є Identifier (значення не винесені у змінну);
|
|
494
515
|
* - або це Identifier, але перед запитом немає guard `if (empty) throw`.
|
|
495
|
-
*
|
|
496
516
|
* @param {Record<string, unknown>} node TemplateLiteral
|
|
497
517
|
* @param {unknown[]} ancestors ancestors від walkAstWithAncestors
|
|
498
518
|
* @param {string} content вихідний код
|
|
@@ -562,7 +582,6 @@ export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan
|
|
|
562
582
|
* Знаходить підстановки списків у `IN (${...})`, які:
|
|
563
583
|
* - не винесені в окрему змінну (в `${...}` стоїть не Identifier);
|
|
564
584
|
* - або винесені, але перед запитом немає перевірки на пустоту з `throw`.
|
|
565
|
-
*
|
|
566
585
|
* @param {string} content вихідний код
|
|
567
586
|
* @param {string} [virtualPath] шлях для вибору мови парсера (lang)
|
|
568
587
|
* @returns {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} список порушень
|