@nitra/cursor 1.8.156 → 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 +3 -9
- 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 +49 -29
- 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 +89 -44
- 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)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (порядок не важливий, кілька викликів зливаються в один список).
|
|
12
12
|
*
|
|
13
13
|
* Обидва контракти можна точково «приглушити» коментарем-маркером
|
|
14
|
-
* `//
|
|
14
|
+
* `// \@nitra/cursor ignore-next-line checkEnv` на рядку безпосередньо перед
|
|
15
15
|
* порушенням — це залишається сумісним escape-hatch для legacy-коду.
|
|
16
16
|
*
|
|
17
17
|
* Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
|
|
@@ -129,7 +129,7 @@ function hasCheckEnvImport(programNode) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* Чи закритий рядок ignore-коментарем `//
|
|
132
|
+
* Чи закритий рядок ignore-коментарем `// \@nitra/cursor ignore-next-line checkEnv`.
|
|
133
133
|
* @param {string[]} lines рядки файлу (split за \n, без CR)
|
|
134
134
|
* @param {number} oneBasedLine 1-based номер рядка з порушенням
|
|
135
135
|
* @returns {boolean} true, якщо попередній рядок містить маркер
|
|
@@ -161,6 +161,51 @@ function isEnvIdentifierMember(node) {
|
|
|
161
161
|
* }} EnvViolation
|
|
162
162
|
*/
|
|
163
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
|
+
}
|
|
208
|
+
|
|
164
209
|
/**
|
|
165
210
|
* Перебирає AST і для кожного знайденого доступу до `process.env` чи `env`
|
|
166
211
|
* (де `env` — імпорт з `@nitra/check-env`) реєструє порушення відповідного типу.
|
|
@@ -191,34 +236,42 @@ function collectViolations(program, content, lines, checkedNames, envFromCheckEn
|
|
|
191
236
|
out.push({ kind, name, line })
|
|
192
237
|
}
|
|
193
238
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
parent.init === node &&
|
|
207
|
-
parent.id &&
|
|
208
|
-
parent.id.type === 'ObjectPattern' &&
|
|
209
|
-
Array.isArray(parent.id.properties)
|
|
210
|
-
) {
|
|
211
|
-
for (const p of parent.id.properties) {
|
|
212
|
-
const name = staticPropertyName(p)
|
|
213
|
-
if (name) report('process-env', name, offsetToLine(content, p.start ?? parent.start))
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return
|
|
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))
|
|
217
251
|
}
|
|
252
|
+
}
|
|
218
253
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}
|
|
221
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Обробка вузлів, що стосуються `env` з `@nitra/check-env`.
|
|
272
|
+
* @param {Record<string, unknown>} node AST-вузол
|
|
273
|
+
*/
|
|
274
|
+
function handleCheckEnvAccess(node) {
|
|
222
275
|
if (isEnvIdentifierMember(node)) {
|
|
223
276
|
const envName = envNameFromMember(node)
|
|
224
277
|
if (envName && !checkedNames.has(envName)) {
|
|
@@ -226,24 +279,17 @@ function collectViolations(program, content, lines, checkedNames, envFromCheckEn
|
|
|
226
279
|
}
|
|
227
280
|
return
|
|
228
281
|
}
|
|
282
|
+
if (isEnvObjectPatternDeclarator(node)) {
|
|
283
|
+
reportObjectPatternKeys(node, 'check-env-missing-checkEnv', name => checkedNames.has(name))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
229
286
|
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
node
|
|
233
|
-
|
|
234
|
-
node.init.type === 'Identifier' &&
|
|
235
|
-
node.init.name === 'env' &&
|
|
236
|
-
node.id &&
|
|
237
|
-
node.id.type === 'ObjectPattern' &&
|
|
238
|
-
Array.isArray(node.id.properties)
|
|
239
|
-
) {
|
|
240
|
-
for (const p of node.id.properties) {
|
|
241
|
-
const name = staticPropertyName(p)
|
|
242
|
-
if (name && !checkedNames.has(name)) {
|
|
243
|
-
report('check-env-missing-checkEnv', name, offsetToLine(content, p.start ?? node.start))
|
|
244
|
-
}
|
|
245
|
-
}
|
|
287
|
+
walkAstWithAncestors(program, [], (node, ancestors) => {
|
|
288
|
+
if (isProcessEnvAccess(node)) {
|
|
289
|
+
handleProcessEnv(node, ancestors)
|
|
290
|
+
return
|
|
246
291
|
}
|
|
292
|
+
if (envFromCheckEnv) handleCheckEnvAccess(node)
|
|
247
293
|
})
|
|
248
294
|
|
|
249
295
|
return out
|
|
@@ -266,7 +312,6 @@ function staticPropertyName(property) {
|
|
|
266
312
|
|
|
267
313
|
/**
|
|
268
314
|
* Знаходить порушення правила «process.env / CheckEnv» у файлі.
|
|
269
|
-
*
|
|
270
315
|
* @param {string} content вихідний код
|
|
271
316
|
* @param {string} [virtualPath] шлях для вибору `lang` парсера
|
|
272
317
|
* @returns {EnvViolation[]} список порушень із типом, іменем змінної та рядком
|
|
@@ -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 }[]} список порушень
|