@kernlang/review 3.1.9 → 3.2.3
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/dist/cache.js +4 -0
- package/dist/cache.js.map +1 -1
- package/dist/file-context.d.ts +6 -0
- package/dist/file-context.js +6 -1
- package/dist/file-context.js.map +1 -1
- package/dist/rules/a11y.d.ts +10 -0
- package/dist/rules/a11y.js +294 -0
- package/dist/rules/a11y.js.map +1 -0
- package/dist/rules/async.d.ts +8 -0
- package/dist/rules/async.js +154 -0
- package/dist/rules/async.js.map +1 -0
- package/dist/rules/index.d.ts +12 -0
- package/dist/rules/index.js +283 -4
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ink.js +41 -0
- package/dist/rules/ink.js.map +1 -1
- package/dist/rules/kern-source.js +94 -14
- package/dist/rules/kern-source.js.map +1 -1
- package/dist/rules/nextjs-app-router.d.ts +11 -0
- package/dist/rules/nextjs-app-router.js +277 -0
- package/dist/rules/nextjs-app-router.js.map +1 -0
- package/dist/rules/nextjs.js +77 -1
- package/dist/rules/nextjs.js.map +1 -1
- package/dist/rules/perf.d.ts +11 -0
- package/dist/rules/perf.js +131 -0
- package/dist/rules/perf.js.map +1 -0
- package/dist/rules/react-composition.d.ts +12 -0
- package/dist/rules/react-composition.js +360 -0
- package/dist/rules/react-composition.js.map +1 -0
- package/dist/rules/react-hooks.d.ts +11 -0
- package/dist/rules/react-hooks.js +380 -0
- package/dist/rules/react-hooks.js.map +1 -0
- package/dist/rules/security-v5.d.ts +11 -0
- package/dist/rules/security-v5.js +200 -0
- package/dist/rules/security-v5.js.map +1 -0
- package/dist/rules/utils.d.ts +16 -0
- package/dist/rules/utils.js +46 -0
- package/dist/rules/utils.js.map +1 -1
- package/dist/taint-ast.js +32 -6
- package/dist/taint-ast.js.map +1 -1
- package/dist/taint-findings.js +3 -0
- package/dist/taint-findings.js.map +1 -1
- package/dist/taint-types.d.ts +2 -2
- package/dist/taint-types.js +38 -4
- package/dist/taint-types.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/cache.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
+
// Version stamp for cache invalidation — changes when rules/analyzers change
|
|
6
|
+
const REVIEW_CACHE_VERSION = '3.2.0';
|
|
5
7
|
export class ReviewCache {
|
|
6
8
|
l1 = new Map();
|
|
7
9
|
cacheDir;
|
|
@@ -66,6 +68,8 @@ export class ReviewCache {
|
|
|
66
68
|
}
|
|
67
69
|
export function computeCacheKey(fileContent, config, filePath) {
|
|
68
70
|
const hash = createHash('sha256');
|
|
71
|
+
// Include version so cache auto-invalidates when kern-lang is upgraded
|
|
72
|
+
hash.update(REVIEW_CACHE_VERSION);
|
|
69
73
|
hash.update(fileContent);
|
|
70
74
|
hash.update(JSON.stringify(config));
|
|
71
75
|
hash.update(filePath);
|
package/dist/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC7F,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,6EAA6E;AAC7E,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAErC,MAAM,OAAO,WAAW;IACd,EAAE,GAAG,IAAI,GAAG,EAAwB,CAAC;IACrC,QAAQ,CAAS;IAEzB;QACE,wGAAwG;QACxG,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;QAClD,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAEO,cAAc;QACpB,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;QAC3E,CAAC;IACH,CAAC;IAEM,GAAG,CAAC,GAAW;QACpB,WAAW;QACX,IAAI,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAED,WAAW;QACX,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QACrD,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1D,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACvB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,GAAG,CAAC,GAAW,EAAE,MAAoB;QAC1C,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC;YACH,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;IAEM,KAAK;QACV,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QAChB,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxD,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAoB,EAAE,QAAgB;IACzF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,uEAAuE;IACvE,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAClC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtB,uFAAuF;IACvF,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBACpB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;wBACrC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAC5B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;wBACvD,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,0BAA0B;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAE7C,MAAM,UAAU,gBAAgB;IAC9B,WAAW,CAAC,KAAK,EAAE,CAAC;AACtB,CAAC"}
|
package/dist/file-context.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* A file imported by both server and client entry points is 'shared'.
|
|
10
10
|
*/
|
|
11
11
|
import type { FileContext, GraphResult } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Check if a file starts with a `'use client'` directive.
|
|
14
|
+
* Exported so App Router rules can detect client-boundary files without
|
|
15
|
+
* going through the full graph pass. Results are memoized across calls.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasUseClientDirective(filePath: string): boolean;
|
|
12
18
|
/**
|
|
13
19
|
* Build FileContext for every file in the import graph.
|
|
14
20
|
* This is the main function — call it once after resolving the import graph,
|
package/dist/file-context.js
CHANGED
|
@@ -46,7 +46,12 @@ function classifyEntryPoint(filePath) {
|
|
|
46
46
|
}
|
|
47
47
|
// ── 'use client' Detection ──────────────────────────────────────────────
|
|
48
48
|
const useClientCache = new Map();
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Check if a file starts with a `'use client'` directive.
|
|
51
|
+
* Exported so App Router rules can detect client-boundary files without
|
|
52
|
+
* going through the full graph pass. Results are memoized across calls.
|
|
53
|
+
*/
|
|
54
|
+
export function hasUseClientDirective(filePath) {
|
|
50
55
|
const cached = useClientCache.get(filePath);
|
|
51
56
|
if (cached !== undefined)
|
|
52
57
|
return cached;
|
package/dist/file-context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-context.js","sourceRoot":"","sources":["../src/file-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAGlC,2EAA2E;AAE3E,2DAA2D;AAC3D,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IAErC,iCAAiC;IACjC,IAAI,oCAAoC,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnE,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,gCAAgC,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IAEtE,mCAAmC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/C,IAAI,4BAA4B,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACP,+BAA+B;IACjC,CAAC;IAED,0FAA0F;IAC1F,IAAI,4EAA4E,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE9G,8BAA8B;IAC9B,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEjD,mBAAmB;IACnB,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE5E,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,2EAA2E;AAE3E,MAAM,cAAc,GAAG,IAAI,GAAG,EAAmB,CAAC;AAElD,
|
|
1
|
+
{"version":3,"file":"file-context.js","sourceRoot":"","sources":["../src/file-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility rules (WCAG 2.1 high-signal subset).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors eslint-plugin-jsx-a11y rule shapes but kept intentionally small —
|
|
5
|
+
* only the checks that have a clear right answer and low false-positive rate.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReviewFinding, RuleContext } from '../types.js';
|
|
8
|
+
declare function imgMissingAlt(ctx: RuleContext): ReviewFinding[];
|
|
9
|
+
export declare const a11yRules: (typeof imgMissingAlt)[];
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility rules (WCAG 2.1 high-signal subset).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors eslint-plugin-jsx-a11y rule shapes but kept intentionally small —
|
|
5
|
+
* only the checks that have a clear right answer and low false-positive rate.
|
|
6
|
+
*/
|
|
7
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
8
|
+
import { finding, insertAfterSpan } from './utils.js';
|
|
9
|
+
// ARIA 1.2 valid role values (subset covering the common ones).
|
|
10
|
+
// Source: https://www.w3.org/TR/wai-aria-1.2/#role_definitions
|
|
11
|
+
const VALID_ROLES = new Set([
|
|
12
|
+
// Landmark
|
|
13
|
+
'banner',
|
|
14
|
+
'complementary',
|
|
15
|
+
'contentinfo',
|
|
16
|
+
'form',
|
|
17
|
+
'main',
|
|
18
|
+
'navigation',
|
|
19
|
+
'region',
|
|
20
|
+
'search',
|
|
21
|
+
// Document structure
|
|
22
|
+
'article',
|
|
23
|
+
'cell',
|
|
24
|
+
'columnheader',
|
|
25
|
+
'definition',
|
|
26
|
+
'directory',
|
|
27
|
+
'document',
|
|
28
|
+
'feed',
|
|
29
|
+
'figure',
|
|
30
|
+
'group',
|
|
31
|
+
'heading',
|
|
32
|
+
'img',
|
|
33
|
+
'list',
|
|
34
|
+
'listitem',
|
|
35
|
+
'math',
|
|
36
|
+
'none',
|
|
37
|
+
'note',
|
|
38
|
+
'presentation',
|
|
39
|
+
'row',
|
|
40
|
+
'rowgroup',
|
|
41
|
+
'rowheader',
|
|
42
|
+
'separator',
|
|
43
|
+
'table',
|
|
44
|
+
'term',
|
|
45
|
+
'toolbar',
|
|
46
|
+
'tooltip',
|
|
47
|
+
// Widget
|
|
48
|
+
'alert',
|
|
49
|
+
'alertdialog',
|
|
50
|
+
'application',
|
|
51
|
+
'button',
|
|
52
|
+
'checkbox',
|
|
53
|
+
'combobox',
|
|
54
|
+
'dialog',
|
|
55
|
+
'grid',
|
|
56
|
+
'gridcell',
|
|
57
|
+
'link',
|
|
58
|
+
'log',
|
|
59
|
+
'marquee',
|
|
60
|
+
'menu',
|
|
61
|
+
'menubar',
|
|
62
|
+
'menuitem',
|
|
63
|
+
'menuitemcheckbox',
|
|
64
|
+
'menuitemradio',
|
|
65
|
+
'option',
|
|
66
|
+
'progressbar',
|
|
67
|
+
'radio',
|
|
68
|
+
'radiogroup',
|
|
69
|
+
'scrollbar',
|
|
70
|
+
'searchbox',
|
|
71
|
+
'slider',
|
|
72
|
+
'spinbutton',
|
|
73
|
+
'status',
|
|
74
|
+
'switch',
|
|
75
|
+
'tab',
|
|
76
|
+
'tablist',
|
|
77
|
+
'tabpanel',
|
|
78
|
+
'textbox',
|
|
79
|
+
'timer',
|
|
80
|
+
'tree',
|
|
81
|
+
'treegrid',
|
|
82
|
+
'treeitem',
|
|
83
|
+
// Live region
|
|
84
|
+
'log',
|
|
85
|
+
'marquee',
|
|
86
|
+
'status',
|
|
87
|
+
'timer',
|
|
88
|
+
]);
|
|
89
|
+
const INTERACTIVE_EVENTS = new Set([
|
|
90
|
+
'onClick',
|
|
91
|
+
'onKeyDown',
|
|
92
|
+
'onKeyUp',
|
|
93
|
+
'onKeyPress',
|
|
94
|
+
'onMouseDown',
|
|
95
|
+
'onMouseUp',
|
|
96
|
+
'onTouchStart',
|
|
97
|
+
'onTouchEnd',
|
|
98
|
+
]);
|
|
99
|
+
const NON_INTERACTIVE_ELEMENTS = new Set(['div', 'span', 'section', 'article', 'li', 'p', 'td', 'th']);
|
|
100
|
+
function getTagName(el) {
|
|
101
|
+
return el.getTagNameNode().getText();
|
|
102
|
+
}
|
|
103
|
+
function getAttr(el, name) {
|
|
104
|
+
for (const attr of el.getAttributes()) {
|
|
105
|
+
if (Node.isJsxAttribute(attr) && attr.getNameNode().getText() === name) {
|
|
106
|
+
return attr;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function hasAttr(el, name) {
|
|
112
|
+
return getAttr(el, name) !== undefined;
|
|
113
|
+
}
|
|
114
|
+
function hasAnyAttr(el, names) {
|
|
115
|
+
return names.some((n) => hasAttr(el, n));
|
|
116
|
+
}
|
|
117
|
+
function* iterJsxElements(ctx) {
|
|
118
|
+
for (const el of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)) {
|
|
119
|
+
yield el;
|
|
120
|
+
}
|
|
121
|
+
for (const el of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)) {
|
|
122
|
+
yield el;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── Rule: img-missing-alt ────────────────────────────────────────────────
|
|
126
|
+
function imgMissingAlt(ctx) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
for (const el of iterJsxElements(ctx)) {
|
|
129
|
+
const tag = getTagName(el);
|
|
130
|
+
// Cover both <img> and next/image <Image>
|
|
131
|
+
if (tag !== 'img' && tag !== 'Image')
|
|
132
|
+
continue;
|
|
133
|
+
// alt="" is valid (decorative image); only flag missing alt
|
|
134
|
+
if (hasAttr(el, 'alt'))
|
|
135
|
+
continue;
|
|
136
|
+
// Role presentation / none exempts from alt requirement
|
|
137
|
+
const role = getAttr(el, 'role')?.getInitializer();
|
|
138
|
+
if (role &&
|
|
139
|
+
Node.isStringLiteral(role) &&
|
|
140
|
+
(role.getLiteralValue() === 'presentation' || role.getLiteralValue() === 'none')) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// aria-hidden="true" also exempts
|
|
144
|
+
if (hasAttr(el, 'aria-hidden'))
|
|
145
|
+
continue;
|
|
146
|
+
// Autofix: insert `alt=""` immediately after the tag name. `alt=""` is
|
|
147
|
+
// the correct default for images whose intent we can't determine — it
|
|
148
|
+
// marks them as decorative so screen readers skip them. The user can
|
|
149
|
+
// replace the empty string with real alt text later.
|
|
150
|
+
findings.push(finding('img-missing-alt', 'error', 'bug', `<${tag}> is missing an alt attribute — screen readers will skip or read the filename`, ctx.filePath, el.getStartLineNumber(), 1, {
|
|
151
|
+
suggestion: 'Add alt="description" for meaningful images, or alt="" for decorative images',
|
|
152
|
+
autofix: {
|
|
153
|
+
type: 'insert-after',
|
|
154
|
+
span: insertAfterSpan(el.getTagNameNode(), ctx.filePath),
|
|
155
|
+
replacement: ' alt=""',
|
|
156
|
+
description: 'Insert alt="" (safe default for unknown-intent images)',
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
162
|
+
// ── Rule: button-missing-name ────────────────────────────────────────────
|
|
163
|
+
function buttonMissingName(ctx) {
|
|
164
|
+
const findings = [];
|
|
165
|
+
for (const el of iterJsxElements(ctx)) {
|
|
166
|
+
const tag = getTagName(el);
|
|
167
|
+
if (tag !== 'button')
|
|
168
|
+
continue;
|
|
169
|
+
// Accept any form of accessible name
|
|
170
|
+
if (hasAnyAttr(el, ['aria-label', 'aria-labelledby', 'title']))
|
|
171
|
+
continue;
|
|
172
|
+
// Self-closing <button /> is always unnamed
|
|
173
|
+
if (Node.isJsxSelfClosingElement(el)) {
|
|
174
|
+
findings.push(finding('button-missing-name', 'error', 'bug', '<button /> has no accessible name — add text children, aria-label, or aria-labelledby', ctx.filePath, el.getStartLineNumber(), 1, { suggestion: 'Add text children, aria-label="Close", or reference a label with aria-labelledby' }));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// For <button>...</button> check children for text / svg with title
|
|
178
|
+
const parent = el.getParent();
|
|
179
|
+
if (!parent)
|
|
180
|
+
continue;
|
|
181
|
+
if (!Node.isJsxElement(parent))
|
|
182
|
+
continue;
|
|
183
|
+
const children = parent.getJsxChildren();
|
|
184
|
+
let hasTextLikeContent = false;
|
|
185
|
+
for (const child of children) {
|
|
186
|
+
if (Node.isJsxText(child) && child.getText().trim().length > 0) {
|
|
187
|
+
hasTextLikeContent = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (Node.isJsxExpression(child)) {
|
|
191
|
+
// Any expression child counts — we can't prove it's empty without eval
|
|
192
|
+
hasTextLikeContent = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
if (Node.isJsxElement(child) || Node.isJsxSelfClosingElement(child)) {
|
|
196
|
+
// Nested element might contain accessible content — don't flag
|
|
197
|
+
hasTextLikeContent = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!hasTextLikeContent) {
|
|
202
|
+
findings.push(finding('button-missing-name', 'error', 'bug', '<button> has no accessible name — empty children and no aria-label', ctx.filePath, el.getStartLineNumber(), 1, { suggestion: 'Add text children, aria-label="Close", or reference a label with aria-labelledby' }));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
// ── Rule: label-missing-for ──────────────────────────────────────────────
|
|
208
|
+
function labelMissingFor(ctx) {
|
|
209
|
+
const findings = [];
|
|
210
|
+
for (const el of iterJsxElements(ctx)) {
|
|
211
|
+
const tag = getTagName(el);
|
|
212
|
+
if (tag !== 'label')
|
|
213
|
+
continue;
|
|
214
|
+
if (hasAnyAttr(el, ['htmlFor', 'for']))
|
|
215
|
+
continue;
|
|
216
|
+
// <label><input /></label> — nested control is fine
|
|
217
|
+
const parent = el.getParent();
|
|
218
|
+
if (parent && Node.isJsxElement(parent)) {
|
|
219
|
+
const hasNestedControl = parent.getDescendants().some((d) => {
|
|
220
|
+
if (!Node.isJsxSelfClosingElement(d) && !Node.isJsxOpeningElement(d))
|
|
221
|
+
return false;
|
|
222
|
+
const name = d.getTagNameNode().getText();
|
|
223
|
+
return name === 'input' || name === 'select' || name === 'textarea';
|
|
224
|
+
});
|
|
225
|
+
if (hasNestedControl)
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
findings.push(finding('label-missing-for', 'warning', 'bug', '<label> is not associated with a form control — add htmlFor={id} or nest the control inside the label', ctx.filePath, el.getStartLineNumber(), 1, { suggestion: 'Add htmlFor="input-id" matching the id prop of the associated input' }));
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
232
|
+
// ── Rule: aria-invalid-role ──────────────────────────────────────────────
|
|
233
|
+
function ariaInvalidRole(ctx) {
|
|
234
|
+
const findings = [];
|
|
235
|
+
for (const el of iterJsxElements(ctx)) {
|
|
236
|
+
const roleAttr = getAttr(el, 'role');
|
|
237
|
+
if (!roleAttr)
|
|
238
|
+
continue;
|
|
239
|
+
const init = roleAttr.getInitializer();
|
|
240
|
+
if (!init || !Node.isStringLiteral(init))
|
|
241
|
+
continue; // Skip expression roles — can't statically validate
|
|
242
|
+
const value = init.getLiteralValue();
|
|
243
|
+
// role can be space-separated fallback list
|
|
244
|
+
const roles = value.split(/\s+/).filter(Boolean);
|
|
245
|
+
for (const r of roles) {
|
|
246
|
+
if (!VALID_ROLES.has(r)) {
|
|
247
|
+
findings.push(finding('aria-invalid-role', 'error', 'bug', `role="${r}" is not a valid ARIA role — assistive tech will ignore the element`, ctx.filePath, roleAttr.getStartLineNumber(), 1, {
|
|
248
|
+
suggestion: `Use a valid ARIA role from the WAI-ARIA 1.2 spec, or remove the role to use the element's implicit role`,
|
|
249
|
+
}));
|
|
250
|
+
break; // one finding per element
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return findings;
|
|
255
|
+
}
|
|
256
|
+
// ── Rule: interactive-noninteractive ─────────────────────────────────────
|
|
257
|
+
function interactiveNonInteractive(ctx) {
|
|
258
|
+
const findings = [];
|
|
259
|
+
for (const el of iterJsxElements(ctx)) {
|
|
260
|
+
const tag = getTagName(el);
|
|
261
|
+
if (!NON_INTERACTIVE_ELEMENTS.has(tag))
|
|
262
|
+
continue;
|
|
263
|
+
let interactiveEvent;
|
|
264
|
+
for (const attr of el.getAttributes()) {
|
|
265
|
+
if (!Node.isJsxAttribute(attr))
|
|
266
|
+
continue;
|
|
267
|
+
const name = attr.getNameNode().getText();
|
|
268
|
+
if (INTERACTIVE_EVENTS.has(name)) {
|
|
269
|
+
interactiveEvent = name;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!interactiveEvent)
|
|
274
|
+
continue;
|
|
275
|
+
// Exempt if role + tabIndex present
|
|
276
|
+
const hasRole = hasAttr(el, 'role');
|
|
277
|
+
const hasTabIndex = hasAttr(el, 'tabIndex') || hasAttr(el, 'tabindex');
|
|
278
|
+
if (hasRole && hasTabIndex)
|
|
279
|
+
continue;
|
|
280
|
+
findings.push(finding('interactive-noninteractive', 'warning', 'bug', `<${tag}> has ${interactiveEvent} but is not keyboard-accessible — keyboard users cannot focus or activate it`, ctx.filePath, el.getStartLineNumber(), 1, {
|
|
281
|
+
suggestion: `Use a <button> element, or add role="button" and tabIndex={0} plus an onKeyDown handler that maps Enter/Space to the same action`,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
return findings;
|
|
285
|
+
}
|
|
286
|
+
// ── Exported a11y rules ──────────────────────────────────────────────────
|
|
287
|
+
export const a11yRules = [
|
|
288
|
+
imgMissingAlt,
|
|
289
|
+
buttonMissingName,
|
|
290
|
+
labelMissingFor,
|
|
291
|
+
ariaInvalidRole,
|
|
292
|
+
interactiveNonInteractive,
|
|
293
|
+
];
|
|
294
|
+
//# sourceMappingURL=a11y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y.js","sourceRoot":"","sources":["../../src/rules/a11y.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async correctness — Wave 2 rules for common Promise/AbortController footguns.
|
|
3
|
+
* Deliberately narrow: each rule fires only on high-confidence patterns.
|
|
4
|
+
*/
|
|
5
|
+
import type { ReviewFinding, RuleContext } from '../types.js';
|
|
6
|
+
declare function promiseAllErrorSwallow(ctx: RuleContext): ReviewFinding[];
|
|
7
|
+
export declare const asyncRules: (typeof promiseAllErrorSwallow)[];
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async correctness — Wave 2 rules for common Promise/AbortController footguns.
|
|
3
|
+
* Deliberately narrow: each rule fires only on high-confidence patterns.
|
|
4
|
+
*/
|
|
5
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
6
|
+
import { finding, insertBeforeSpan } from './utils.js';
|
|
7
|
+
// ── Rule: promise-all-error-swallow ──────────────────────────────────────
|
|
8
|
+
// Promise.all([...]) without .catch and not inside a try/catch is a bug:
|
|
9
|
+
// a single rejection silently cancels the handler and the error is lost.
|
|
10
|
+
function promiseAllErrorSwallow(ctx) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
13
|
+
const callee = call.getExpression().getText();
|
|
14
|
+
if (callee !== 'Promise.all' && callee !== 'Promise.allSettled')
|
|
15
|
+
continue;
|
|
16
|
+
// allSettled is safe by construction (never rejects)
|
|
17
|
+
if (callee === 'Promise.allSettled')
|
|
18
|
+
continue;
|
|
19
|
+
// Walk up to see if we're inside a try block
|
|
20
|
+
let inTry = false;
|
|
21
|
+
let chained = false;
|
|
22
|
+
let cur = call.getParent();
|
|
23
|
+
while (cur) {
|
|
24
|
+
if (Node.isTryStatement(cur)) {
|
|
25
|
+
inTry = true;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
// Function boundary — stop searching for try
|
|
29
|
+
if (Node.isFunctionDeclaration(cur) || Node.isArrowFunction(cur) || Node.isFunctionExpression(cur)) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
cur = cur.getParent();
|
|
33
|
+
}
|
|
34
|
+
// Check for .catch chain: Promise.all(...).catch(...) / .then(..., onRejected)
|
|
35
|
+
const parent = call.getParent();
|
|
36
|
+
if (parent && Node.isPropertyAccessExpression(parent)) {
|
|
37
|
+
const method = parent.getName();
|
|
38
|
+
if (method === 'catch')
|
|
39
|
+
chained = true;
|
|
40
|
+
if (method === 'then') {
|
|
41
|
+
// Check if .then has a second argument (onRejected)
|
|
42
|
+
const thenCall = parent.getParent();
|
|
43
|
+
if (thenCall && Node.isCallExpression(thenCall) && thenCall.getArguments().length >= 2) {
|
|
44
|
+
chained = true;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// See if there's a .catch further along
|
|
48
|
+
let chain = thenCall;
|
|
49
|
+
while (chain && Node.isCallExpression(chain)) {
|
|
50
|
+
const p = chain.getParent();
|
|
51
|
+
if (p && Node.isPropertyAccessExpression(p) && p.getName() === 'catch') {
|
|
52
|
+
chained = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
chain = p?.getParent();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Check if awaited inside an async function — the caller may handle it
|
|
61
|
+
const awaited = parent && Node.isAwaitExpression(parent);
|
|
62
|
+
if (!inTry && !chained && !awaited) {
|
|
63
|
+
findings.push(finding('promise-all-error-swallow', 'warning', 'bug', 'Promise.all() called without .catch, try/catch, or await — a single rejection will be silently unhandled', ctx.filePath, call.getStartLineNumber(), 1, {
|
|
64
|
+
suggestion: 'Add .catch(err => ...), wrap in try/catch inside an async function, or use Promise.allSettled if per-promise failures are expected',
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
// ── Rule: abortcontroller-leak ───────────────────────────────────────────
|
|
71
|
+
// `new AbortController()` created inside useEffect without `.abort()` in
|
|
72
|
+
// the cleanup return. Classic memory leak + stale-response bug.
|
|
73
|
+
const EFFECT_HOOKS = new Set(['useEffect', 'useLayoutEffect']);
|
|
74
|
+
function abortControllerLeak(ctx) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
for (const call of ctx.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
77
|
+
const calleeText = call.getExpression().getText();
|
|
78
|
+
const calleeName = calleeText.includes('.') ? calleeText.split('.').pop() : calleeText;
|
|
79
|
+
if (!EFFECT_HOOKS.has(calleeName))
|
|
80
|
+
continue;
|
|
81
|
+
const args = call.getArguments();
|
|
82
|
+
if (args.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
const fnArg = args[0];
|
|
85
|
+
if (!Node.isArrowFunction(fnArg) && !Node.isFunctionExpression(fnArg))
|
|
86
|
+
continue;
|
|
87
|
+
const body = fnArg.getBody();
|
|
88
|
+
if (!body)
|
|
89
|
+
continue;
|
|
90
|
+
if (!Node.isBlock(body))
|
|
91
|
+
continue;
|
|
92
|
+
// Find `new AbortController()` created directly in the effect body
|
|
93
|
+
const controllers = [];
|
|
94
|
+
for (const newExpr of body.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
95
|
+
if (newExpr.getExpression().getText() !== 'AbortController')
|
|
96
|
+
continue;
|
|
97
|
+
// Walk up to find the variable declaration
|
|
98
|
+
let cur = newExpr.getParent();
|
|
99
|
+
while (cur && !Node.isVariableDeclaration(cur)) {
|
|
100
|
+
cur = cur.getParent();
|
|
101
|
+
}
|
|
102
|
+
if (cur && Node.isVariableDeclaration(cur)) {
|
|
103
|
+
const nameNode = cur.getNameNode();
|
|
104
|
+
if (Node.isIdentifier(nameNode)) {
|
|
105
|
+
controllers.push({ name: nameNode.getText(), line: newExpr.getStartLineNumber() });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (controllers.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
// Check the cleanup function (the return value of the effect body)
|
|
112
|
+
let cleanupText = '';
|
|
113
|
+
let hasExistingReturn = false;
|
|
114
|
+
for (const stmt of body.getStatements()) {
|
|
115
|
+
if (Node.isReturnStatement(stmt)) {
|
|
116
|
+
hasExistingReturn = true;
|
|
117
|
+
const expr = stmt.getExpression();
|
|
118
|
+
if (expr && (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr))) {
|
|
119
|
+
cleanupText = expr.getText();
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const ctrl of controllers) {
|
|
125
|
+
// Require both: the ref name appears in cleanup AND .abort() is called
|
|
126
|
+
const hasAbortCall = new RegExp(`\\b${ctrl.name}\\s*\\.\\s*abort\\s*\\(`).test(cleanupText);
|
|
127
|
+
if (!hasAbortCall) {
|
|
128
|
+
// Autofix: insert a cleanup return immediately before the closing brace
|
|
129
|
+
// of the effect body. Only safe when there is NO existing return — if
|
|
130
|
+
// one is there, the user already has a cleanup and we'd need to merge
|
|
131
|
+
// the abort into it, which is too risky for an automated transform.
|
|
132
|
+
const closingBrace = body.getLastChildByKind(SyntaxKind.CloseBraceToken);
|
|
133
|
+
const canAutofix = !hasExistingReturn && controllers.length === 1 && closingBrace != null;
|
|
134
|
+
findings.push(finding('abortcontroller-leak', 'warning', 'bug', `AbortController '${ctrl.name}' created in ${calleeName} but never aborted in cleanup — in-flight requests survive unmount and may overwrite newer state`, ctx.filePath, ctrl.line, 1, {
|
|
135
|
+
suggestion: `Return a cleanup function that calls ${ctrl.name}.abort(): return () => ${ctrl.name}.abort();`,
|
|
136
|
+
...(canAutofix && closingBrace
|
|
137
|
+
? {
|
|
138
|
+
autofix: {
|
|
139
|
+
type: 'insert-before',
|
|
140
|
+
span: insertBeforeSpan(closingBrace, ctx.filePath),
|
|
141
|
+
replacement: ` return () => ${ctrl.name}.abort();\n`,
|
|
142
|
+
description: `Insert cleanup return that aborts ${ctrl.name}`,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
// ── Exported Async Rules ─────────────────────────────────────────────────
|
|
153
|
+
export const asyncRules = [promiseAllErrorSwallow, abortControllerLeak];
|
|
154
|
+
//# sourceMappingURL=async.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async.js","sourceRoot":"","sources":["../../src/rules/async.ts"],"names":[],"mappings":"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"}
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -24,6 +24,18 @@ export interface RuleInfo {
|
|
|
24
24
|
layer: string;
|
|
25
25
|
severity: 'error' | 'warning' | 'info';
|
|
26
26
|
description: string;
|
|
27
|
+
/**
|
|
28
|
+
* Precision hint used by kern-sight to stratify rules in the sidebar.
|
|
29
|
+
* 'high' — rule has strong substrate (taint graph, file-context, ground-truth AST); ship on.
|
|
30
|
+
* 'medium' — rule relies on heuristics; consider hide-by-default after first scan.
|
|
31
|
+
* 'experimental' — rule may be noisy; hide-by-default, promote after signal data proves it out.
|
|
32
|
+
*/
|
|
33
|
+
precision?: 'high' | 'medium' | 'experimental';
|
|
34
|
+
/**
|
|
35
|
+
* Wave number in the rollout plan. Used by kern-sight to group new rules
|
|
36
|
+
* visually and by `--list-rules` to surface what just landed. Wave 0 = substrate.
|
|
37
|
+
*/
|
|
38
|
+
rolloutPhase?: number;
|
|
27
39
|
}
|
|
28
40
|
/**
|
|
29
41
|
* Get the rule registry, optionally filtered by target.
|