@atlisp/lint 0.1.17 → 0.1.19

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.
Files changed (38) hide show
  1. package/README.md +3 -4
  2. package/atlisp-lint.default.json +12 -5
  3. package/dist/checks/bare-names.js +2 -0
  4. package/dist/checks/constant-condition.js +3 -0
  5. package/dist/checks/empty-catch.js +68 -23
  6. package/dist/checks/entget-in-loop.d.ts +3 -0
  7. package/dist/checks/entget-in-loop.js +56 -0
  8. package/dist/checks/if-without-else.d.ts +4 -0
  9. package/dist/checks/if-without-else.js +31 -0
  10. package/dist/checks/long-function-call.js +1 -1
  11. package/dist/checks/promise-handler.d.ts +4 -0
  12. package/dist/checks/promise-handler.js +53 -0
  13. package/dist/checks/redundant-list.d.ts +4 -0
  14. package/dist/checks/redundant-list.js +22 -0
  15. package/dist/checks/string-concat-loop.d.ts +3 -0
  16. package/dist/checks/string-concat-loop.js +59 -0
  17. package/dist/checks/undeclared-setq.js +2 -0
  18. package/dist/checks/unused-catch-result.d.ts +3 -0
  19. package/dist/checks/unused-catch-result.js +28 -0
  20. package/dist/config.js +11 -4
  21. package/dist/formatters-html.d.ts +3 -0
  22. package/dist/formatters-html.js +91 -0
  23. package/dist/formatters-sarif.d.ts +3 -0
  24. package/dist/formatters-sarif.js +55 -0
  25. package/dist/index.js +41 -2
  26. package/dist/locale.js +23 -7
  27. package/dist/presets.js +16 -2
  28. package/dist/project.js +180 -2
  29. package/dist/rules.js +8 -1
  30. package/dist/runner.d.ts +1 -2
  31. package/dist/runner.js +106 -224
  32. package/dist/validate.js +8 -1
  33. package/dist/visitor-runner.d.ts +4 -0
  34. package/dist/visitor-runner.js +709 -0
  35. package/package.json +1 -1
  36. package/dist/atlisp-lint.default.json +0 -90
  37. package/dist/lib/lint-sbcl.lisp +0 -161
  38. package/dist/stub-packages.json +0 -41
package/README.md CHANGED
@@ -92,7 +92,6 @@ npx @atlisp/lint --format-check
92
92
  | `constant_condition` | warn | 风格 | if/while/cond 中常量条件 |
93
93
  | `redundant_progn` | warn | 风格 | 分支中多余 progn |
94
94
  | `empty_branch` | warn | 风格 | if/cond 中空分支 |
95
- | `redundant_cond` | warn | 风格 | 单子句 cond 或末尾 T 子句 |
96
95
  | `redundant_if` | warn | 风格 | if/when 中冗余 progn |
97
96
  | `redundant_let` | warn | 风格 | 无绑定 let 建议改用 progn |
98
97
  | `redundant_quotes` | warn | 风格 | 冗余双引号 ''x |
@@ -232,12 +231,12 @@ npx @atlisp/lint --format-check
232
231
  },
233
232
  "preset": "recommended",
234
233
  "line_length": {
235
- "max": 100,
234
+ "max": 200,
236
235
  "tab_width": 2
237
236
  },
238
237
  "function_complexity": {
239
- "max_lines": 60,
240
- "max_nesting": 6
238
+ "max_lines": 300,
239
+ "max_nesting": 30
241
240
  },
242
241
  "cl_syntax": {
243
242
  "keywords": ["&key"]
@@ -31,7 +31,6 @@
31
31
  "unused_let_binding": "warn",
32
32
  "recursive_call": "warn",
33
33
  "variable_shadow": "warn",
34
- "redundant_cond": "warn",
35
34
  "unused_local_fun": "warn",
36
35
  "multiple_setq": "warn",
37
36
  "redundant_quotes": "warn",
@@ -80,15 +79,23 @@
80
79
  "loop_optimization": "off",
81
80
  "type_check": "off",
82
81
  "redundant_if": "warn",
83
- "undeclared_setq": "warn"
82
+ "undeclared_setq": "warn",
83
+ "unused_catch_result": "warn",
84
+ "string_concat_loop": "warn",
85
+ "if_without_else": "warn",
86
+ "redundant_list": "warn",
87
+ "entget_in_loop": "warn",
88
+ "promise_handler": "warn",
89
+ "module_cycle": "warn",
90
+ "arg_count_project": "warn"
84
91
  },
85
92
  "line_length": {
86
- "max": 100,
93
+ "max": 200,
87
94
  "tab_width": 2
88
95
  },
89
96
  "function_complexity": {
90
- "max_lines": 60,
91
- "max_nesting": 15
97
+ "max_lines": 300,
98
+ "max_nesting": 30
92
99
  },
93
100
  "cl_syntax": {
94
101
  "keywords": ["&key"]
@@ -18,6 +18,8 @@ function checkBareFunctionNames(content, file, allowlist, namespacePattern) {
18
18
  continue; // local: defun foo/bar
19
19
  if (name.startsWith('cb-'))
20
20
  continue; // DCL callback
21
+ if (name === '*error*')
22
+ continue;
21
23
  if (!nsRegex.test(name) && !name.includes(':')) {
22
24
  issues.push({
23
25
  file,
@@ -41,6 +41,9 @@ function checkConstantConditionAst(ast, file) {
41
41
  continue;
42
42
  const condTest = clause.children[0];
43
43
  if (condTest.type === 'symbol' && condTest.name && CONSTANTS.has(condTest.name)) {
44
+ // Skip last clause with T — that's the standard else/default pattern
45
+ if (condTest.name === 'T' && i === condNode.children.length - 1)
46
+ continue;
44
47
  issues.push({
45
48
  file,
46
49
  line: condTest.pos.line,
@@ -2,33 +2,78 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkEmptyCatch = checkEmptyCatch;
4
4
  const locale_1 = require("../locale");
5
- const utils_1 = require("../utils");
5
+ const parser_1 = require("@atlisp/parser");
6
6
  function checkEmptyCatch(content, file) {
7
7
  const issues = [];
8
- const lines = content.split('\n');
9
- for (let i = 0; i < lines.length; i++) {
10
- const stripped = (0, utils_1.stripLine)(lines[i]);
11
- // Detect (vl-catch-all-apply ...) not inside (vl-catch-all-error-p ...) check
12
- if (stripped.includes('vl-catch-all-apply')) {
13
- // Check subsequent lines for error-p check
14
- let hasErrorCheck = false;
15
- for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
16
- if ((0, utils_1.stripLine)(lines[j]).includes('vl-catch-all-error-p')) {
17
- hasErrorCheck = true;
18
- break;
19
- }
20
- }
21
- if (!hasErrorCheck) {
22
- issues.push({
23
- file,
24
- line: i + 1,
25
- severity: 'warn',
26
- rule: 'empty_catch',
27
- message: (0, locale_1.t)('empty_catch'),
28
- });
29
- }
8
+ const ast = (0, parser_1.parseAst)(content);
9
+ const catchNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'vl-catch-all-apply'));
10
+ for (const node of catchNodes) {
11
+ if (isInQuote(node))
12
+ continue;
13
+ if (isWrappedByCatchError(node))
14
+ continue;
15
+ if (isSetqStored(node)) {
16
+ const varName = getSetqVarName(node);
17
+ if (varName && hasErrorCheckInSiblings(node, varName))
18
+ continue;
30
19
  }
20
+ issues.push({
21
+ file,
22
+ line: node.pos.line,
23
+ severity: 'warn',
24
+ rule: 'empty_catch',
25
+ message: (0, locale_1.t)('empty_catch'),
26
+ });
31
27
  }
32
28
  return issues;
33
29
  }
30
+ function isInQuote(node) {
31
+ let p = node.parent;
32
+ while (p) {
33
+ if ((0, parser_1.astIsList)(p, 'quote'))
34
+ return true;
35
+ p = p.parent;
36
+ }
37
+ return false;
38
+ }
39
+ function isWrappedByCatchError(node) {
40
+ const parent = node.parent;
41
+ return !!(parent && (0, parser_1.astIsList)(parent, 'vl-catch-all-error-p'));
42
+ }
43
+ function isSetqStored(node) {
44
+ const parent = node.parent;
45
+ if (!parent || !(0, parser_1.astIsList)(parent, 'setq'))
46
+ return false;
47
+ if (!parent.children || parent.children.length < 3)
48
+ return false;
49
+ const idx = parent.children.indexOf(node);
50
+ return idx >= 2 && idx % 2 === 0;
51
+ }
52
+ function getSetqVarName(node) {
53
+ const parent = node.parent;
54
+ if (!parent || !parent.children)
55
+ return null;
56
+ const idx = parent.children.indexOf(node);
57
+ const varNode = parent.children[idx - 1];
58
+ if (varNode && varNode.type === 'symbol' && varNode.name) {
59
+ return varNode.name;
60
+ }
61
+ return null;
62
+ }
63
+ function hasErrorCheckInSiblings(node, varName) {
64
+ const parent = node.parent;
65
+ if (!parent || !parent.children)
66
+ return false;
67
+ const idx = parent.children.indexOf(node);
68
+ for (let i = idx + 1; i < parent.children.length; i++) {
69
+ const sibling = parent.children[i];
70
+ if ((0, parser_1.astIsList)(sibling, 'vl-catch-all-error-p')) {
71
+ if (sibling.children && sibling.children.length >= 2 &&
72
+ (0, parser_1.astIsSymbol)(sibling.children[1], varName)) {
73
+ return true;
74
+ }
75
+ }
76
+ }
77
+ return false;
78
+ }
34
79
  //# sourceMappingURL=empty-catch.js.map
@@ -0,0 +1,3 @@
1
+ import { Issue } from '../types';
2
+ export declare function checkEntgetInLoop(content: string, file: string): Issue[];
3
+ //# sourceMappingURL=entget-in-loop.d.ts.map
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkEntgetInLoop = checkEntgetInLoop;
4
+ const locale_1 = require("../locale");
5
+ function isInsideLoop(ast, targetNode) {
6
+ let node = targetNode.parent;
7
+ while (node) {
8
+ if (node.type === 'list' && node.children && node.children.length > 0 &&
9
+ node.children[0].type === 'symbol') {
10
+ const name = node.children[0].name;
11
+ if (name === 'while' || name === 'repeat' || name === 'foreach') {
12
+ return true;
13
+ }
14
+ }
15
+ node = node.parent;
16
+ }
17
+ return false;
18
+ }
19
+ function checkEntgetInLoop(content, file) {
20
+ const issues = [];
21
+ const lines = content.split('\n');
22
+ const re = /\((while|repeat|foreach)\s/;
23
+ for (let i = 0; i < lines.length; i++) {
24
+ if (re.test(lines[i])) {
25
+ let depth = 0;
26
+ let inLoop = false;
27
+ let entgetCount = 0;
28
+ for (let j = i; j < Math.min(i + 50, lines.length); j++) {
29
+ for (let k = 0; k < lines[j].length; k++) {
30
+ if (lines[j][k] === '(') {
31
+ depth++;
32
+ if (!inLoop)
33
+ inLoop = true;
34
+ }
35
+ else if (lines[j][k] === ')')
36
+ depth--;
37
+ }
38
+ if (lines[j].includes('entget'))
39
+ entgetCount++;
40
+ if (depth === 0 && inLoop)
41
+ break;
42
+ }
43
+ if (entgetCount > 0) {
44
+ issues.push({
45
+ file,
46
+ line: i + 1,
47
+ severity: 'warn',
48
+ rule: 'entget_in_loop',
49
+ message: (0, locale_1.t)('entget_in_loop'),
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return issues;
55
+ }
56
+ //# sourceMappingURL=entget-in-loop.js.map
@@ -0,0 +1,4 @@
1
+ import { Issue } from '../types';
2
+ import { AstNode } from '@atlisp/parser';
3
+ export declare function checkIfWithoutElse(ast: AstNode, file: string): Issue[];
4
+ //# sourceMappingURL=if-without-else.d.ts.map
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkIfWithoutElse = checkIfWithoutElse;
4
+ const locale_1 = require("../locale");
5
+ const parser_1 = require("@atlisp/parser");
6
+ function checkIfWithoutElse(ast, file) {
7
+ const issues = [];
8
+ const ifNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'if'));
9
+ for (const node of ifNodes) {
10
+ if (!node.children || node.children.length < 3)
11
+ continue;
12
+ if (node.children.length === 3) {
13
+ const parent = node.parent;
14
+ if (parent && parent.type === 'list' && parent.children && parent.children.length > 0 &&
15
+ parent.children[0].type === 'symbol') {
16
+ const parentName = parent.children[0].name;
17
+ if (parentName === 'cond' || parentName === 'if')
18
+ continue;
19
+ }
20
+ issues.push({
21
+ file,
22
+ line: node.pos.line,
23
+ severity: 'warn',
24
+ rule: 'if_without_else',
25
+ message: (0, locale_1.t)('if_without_else'),
26
+ });
27
+ }
28
+ }
29
+ return issues;
30
+ }
31
+ //# sourceMappingURL=if-without-else.js.map
@@ -39,7 +39,7 @@ function checkLongFunctionCall(content, file, maxArgs) {
39
39
  }
40
40
  if (inArg)
41
41
  argCount++;
42
- if (argCount > maxArgs && !m[1].startsWith('(') && !m[1].startsWith('"') && m[1] !== 'if') {
42
+ if (argCount > maxArgs && !m[1].startsWith('(') && !m[1].startsWith('"') && m[1] !== 'if' && m[1] !== 'quote') {
43
43
  issues.push({
44
44
  file,
45
45
  line: i + 1,
@@ -0,0 +1,4 @@
1
+ import { Issue } from '../types';
2
+ import { AstNode } from '@atlisp/parser';
3
+ export declare function checkPromiseHandler(ast: AstNode, file: string): Issue[];
4
+ //# sourceMappingURL=promise-handler.d.ts.map
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkPromiseHandler = checkPromiseHandler;
4
+ const locale_1 = require("../locale");
5
+ const parser_1 = require("@atlisp/parser");
6
+ function checkPromiseHandler(ast, file) {
7
+ const issues = [];
8
+ const vlaxNodes = (0, parser_1.astFindAll)(ast, (n) => !!(n.type === 'list' && n.children && n.children.length > 0 &&
9
+ n.children[0].type === 'symbol' && typeof n.children[0].name === 'string' &&
10
+ (n.children[0].name.startsWith('vlax-invoke') || n.children[0].name.startsWith('vlax-invoke-method'))));
11
+ for (const node of vlaxNodes) {
12
+ if (isInQuote(node))
13
+ continue;
14
+ const parent = node.parent;
15
+ if (parent && parent.type === 'list' && parent.children && parent.children.length > 0 &&
16
+ parent.children[0].type === 'symbol' && parent.children[0].name === 'vl-catch-all-apply') {
17
+ continue;
18
+ }
19
+ let hasCallback = false;
20
+ if (node.children) {
21
+ for (const child of node.children) {
22
+ if (child.type === 'list' && child.children && child.children.length > 0 &&
23
+ child.children[0].type === 'symbol' &&
24
+ (child.children[0].name === 'lambda' || child.children[0].name === 'function')) {
25
+ hasCallback = true;
26
+ break;
27
+ }
28
+ }
29
+ }
30
+ if (!hasCallback) {
31
+ issues.push({
32
+ file,
33
+ line: node.pos.line,
34
+ severity: 'warn',
35
+ rule: 'promise_handler',
36
+ message: (0, locale_1.t)('promise_handler'),
37
+ });
38
+ }
39
+ }
40
+ return issues;
41
+ }
42
+ function isInQuote(node) {
43
+ let p = node.parent;
44
+ while (p) {
45
+ if (p.type === 'list' && p.children && p.children.length > 0 &&
46
+ p.children[0].type === 'symbol' && p.children[0].name === 'quote') {
47
+ return true;
48
+ }
49
+ p = p.parent;
50
+ }
51
+ return false;
52
+ }
53
+ //# sourceMappingURL=promise-handler.js.map
@@ -0,0 +1,4 @@
1
+ import { Issue } from '../types';
2
+ import { AstNode } from '@atlisp/parser';
3
+ export declare function checkRedundantList(ast: AstNode, file: string): Issue[];
4
+ //# sourceMappingURL=redundant-list.d.ts.map
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkRedundantList = checkRedundantList;
4
+ const locale_1 = require("../locale");
5
+ const parser_1 = require("@atlisp/parser");
6
+ function checkRedundantList(ast, file) {
7
+ const issues = [];
8
+ const listNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'list'));
9
+ for (const node of listNodes) {
10
+ if (!node.children || node.children.length === 1) {
11
+ issues.push({
12
+ file,
13
+ line: node.pos.line,
14
+ severity: 'warn',
15
+ rule: 'redundant_list',
16
+ message: (0, locale_1.t)('redundant_list'),
17
+ });
18
+ }
19
+ }
20
+ return issues;
21
+ }
22
+ //# sourceMappingURL=redundant-list.js.map
@@ -0,0 +1,3 @@
1
+ import { Issue } from '../types';
2
+ export declare function checkStringConcatInLoop(content: string, file: string): Issue[];
3
+ //# sourceMappingURL=string-concat-loop.d.ts.map
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkStringConcatInLoop = checkStringConcatInLoop;
4
+ const locale_1 = require("../locale");
5
+ const parser_1 = require("@atlisp/parser");
6
+ function hasStrcatInBody(body) {
7
+ for (const node of body) {
8
+ for (const n of (0, parser_1.astWalk)(node)) {
9
+ if (n.type === 'list' && n.children && n.children.length > 0 &&
10
+ n.children[0].type === 'symbol' && n.children[0].name === 'strcat') {
11
+ return true;
12
+ }
13
+ }
14
+ }
15
+ return false;
16
+ }
17
+ function isLoopForm(name) {
18
+ return name === 'while' || name === 'repeat' || name === 'foreach';
19
+ }
20
+ function checkStringConcatInLoop(content, file) {
21
+ const issues = [];
22
+ const lines = content.split('\n');
23
+ const re = /\((while|repeat|foreach)\s/g;
24
+ for (let i = 0; i < lines.length; i++) {
25
+ let match;
26
+ while ((match = re.exec(lines[i])) !== null) {
27
+ const name = match[1];
28
+ let depth = 0;
29
+ let started = false;
30
+ let startLine = i;
31
+ let foundStrcat = false;
32
+ for (let j = i; j < lines.length; j++) {
33
+ for (let k = 0; k < lines[j].length; k++) {
34
+ const ch = lines[j][k];
35
+ if (ch === '(')
36
+ depth++;
37
+ else if (ch === ')')
38
+ depth--;
39
+ }
40
+ if (!started && depth > 0)
41
+ started = true;
42
+ if (depth === 0 && started)
43
+ break;
44
+ }
45
+ const loopBody = lines.slice(i, i + 20).join('\n');
46
+ if (loopBody.includes('strcat') || loopBody.includes('strcat ')) {
47
+ issues.push({
48
+ file,
49
+ line: i + 1,
50
+ severity: 'warn',
51
+ rule: 'string_concat_loop',
52
+ message: (0, locale_1.t)('string_concat_loop', name),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ return issues;
58
+ }
59
+ //# sourceMappingURL=string-concat-loop.js.map
@@ -58,6 +58,8 @@ function checkUndeclaredSetq(content, file) {
58
58
  continue;
59
59
  if (v.name === 'T' || v.name === 'nil')
60
60
  continue;
61
+ if (v.name.includes('::'))
62
+ continue;
61
63
  if (isEarmuffed(v.name))
62
64
  continue;
63
65
  if (declared.has(v.name))
@@ -0,0 +1,3 @@
1
+ import { Issue } from '../types';
2
+ export declare function checkUnusedCatchResult(content: string, file: string): Issue[];
3
+ //# sourceMappingURL=unused-catch-result.d.ts.map
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkUnusedCatchResult = checkUnusedCatchResult;
4
+ const locale_1 = require("../locale");
5
+ function checkUnusedCatchResult(content, file) {
6
+ const issues = [];
7
+ const lines = content.split('\n');
8
+ const catchRe = /\(vl-catch-all-apply\s+'[^)]+\)/g;
9
+ for (let i = 0; i < lines.length; i++) {
10
+ const line = lines[i];
11
+ let match;
12
+ while ((match = catchRe.exec(line)) !== null) {
13
+ const col = match.index;
14
+ const after = line.slice(col + match[0].length).trim();
15
+ if (!after.startsWith(')')) {
16
+ issues.push({
17
+ file,
18
+ line: i + 1,
19
+ severity: 'warn',
20
+ rule: 'unused_catch_result',
21
+ message: (0, locale_1.t)('unused_catch_result'),
22
+ });
23
+ }
24
+ }
25
+ }
26
+ return issues;
27
+ }
28
+ //# sourceMappingURL=unused-catch-result.js.map
package/dist/config.js CHANGED
@@ -74,7 +74,6 @@ const DEFAULT_CONFIG = {
74
74
  unused_let_binding: 'warn',
75
75
  recursive_call: 'warn',
76
76
  variable_shadow: 'warn',
77
- redundant_cond: 'warn',
78
77
  unused_local_fun: 'warn',
79
78
  multiple_setq: 'warn',
80
79
  redundant_quotes: 'warn',
@@ -123,14 +122,22 @@ const DEFAULT_CONFIG = {
123
122
  type_check: 'off',
124
123
  format_indent: 'off',
125
124
  redundant_if: 'warn',
125
+ unused_catch_result: 'warn',
126
+ string_concat_loop: 'warn',
127
+ if_without_else: 'warn',
128
+ redundant_list: 'warn',
129
+ entget_in_loop: 'warn',
130
+ promise_handler: 'warn',
131
+ module_cycle: 'warn',
132
+ arg_count_project: 'warn',
126
133
  },
127
134
  line_length: {
128
- max: 100,
135
+ max: 200,
129
136
  tab_width: 2,
130
137
  },
131
138
  function_complexity: {
132
- max_lines: 60,
133
- max_nesting: 15,
139
+ max_lines: 300,
140
+ max_nesting: 30,
134
141
  },
135
142
  cl_syntax: {
136
143
  keywords: ['&key'],
@@ -0,0 +1,3 @@
1
+ import { Issue } from './types';
2
+ export declare function formatHtml(issues: Issue[], errorCount: number, warningCount: number): string;
3
+ //# sourceMappingURL=formatters-html.d.ts.map
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatHtml = formatHtml;
4
+ function formatHtml(issues, errorCount, warningCount) {
5
+ const summaryColor = errorCount > 0 ? '#dc3545' : warningCount > 0 ? '#ffc107' : '#198754';
6
+ const ruleGroups = new Map();
7
+ for (const iss of issues) {
8
+ const list = ruleGroups.get(iss.rule) || [];
9
+ list.push(iss);
10
+ ruleGroups.set(iss.rule, list);
11
+ }
12
+ const sortedRules = Array.from(ruleGroups.entries()).sort(([a], [b]) => a.localeCompare(b));
13
+ let rows = '';
14
+ for (const iss of issues) {
15
+ const tag = iss.severity === 'error'
16
+ ? '<span class="badge bg-danger">ERROR</span>'
17
+ : '<span class="badge bg-warning text-dark">WARN</span>';
18
+ rows += `<tr><td>${tag}</td><td>${iss.rule}</td><td>${iss.file}</td><td>${iss.line || '-'}</td><td>${escHtml(iss.message)}</td></tr>\n`;
19
+ }
20
+ let summaryHtml = '';
21
+ if (errorCount > 0) {
22
+ summaryHtml += `<span class="text-danger fw-bold">${errorCount} error(s)</span> `;
23
+ }
24
+ if (warningCount > 0) {
25
+ summaryHtml += `<span class="text-warning fw-bold">${warningCount} warning(s)</span> `;
26
+ }
27
+ if (errorCount === 0 && warningCount === 0) {
28
+ summaryHtml = '<span class="text-success fw-bold">All files OK</span>';
29
+ }
30
+ return `<!DOCTYPE html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="UTF-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
+ <title>@atlisp/lint Report</title>
36
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
37
+ <style>
38
+ body { padding: 20px; background: #f8f9fa; }
39
+ .summary-card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
40
+ .summary-count { font-size: 2em; font-weight: bold; margin-right: 20px; }
41
+ table { font-size: 0.9em; }
42
+ .badge { font-size: 0.75em; }
43
+ footer { margin-top: 30px; color: #6c757d; font-size: 0.85em; text-align: center; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div class="container">
48
+ <h1 class="mb-4">@atlisp/lint Report</h1>
49
+
50
+ <div class="summary-card">
51
+ <h5 class="card-title" style="color:${summaryColor}">Summary</h5>
52
+ <p>${summaryHtml}</p>
53
+ <p class="text-muted">${issues.length} total issues across ${ruleGroups.size} rules</p>
54
+ </div>
55
+
56
+ ${sortedRules.map(([rule, ruleIssues]) => `
57
+ <div class="summary-card">
58
+ <h5 class="card-title">${escHtml(rule)} <span class="badge bg-secondary">${ruleIssues.length}</span></h5>
59
+ <ul class="list-group list-group-flush">
60
+ ${ruleIssues.map(iss => `
61
+ <li class="list-group-item d-flex justify-content-between align-items-start">
62
+ <div>
63
+ <span class="fw-bold">${escHtml(iss.file)}:${iss.line || '-'}</span>
64
+ <br><span class="text-muted">${escHtml(iss.message)}</span>
65
+ </div>
66
+ ${iss.severity === 'error' ? '<span class="badge bg-danger rounded-pill">ERROR</span>' : '<span class="badge bg-warning text-dark rounded-pill">WARN</span>'}
67
+ </li>
68
+ `).join('')}
69
+ </ul>
70
+ </div>
71
+ `).join('')}
72
+
73
+ <div class="summary-card">
74
+ <h5>All Issues</h5>
75
+ <div class="table-responsive">
76
+ <table class="table table-striped table-hover">
77
+ <thead><tr><th>Severity</th><th>Rule</th><th>File</th><th>Line</th><th>Message</th></tr></thead>
78
+ <tbody>${rows}</tbody>
79
+ </table>
80
+ </div>
81
+ </div>
82
+
83
+ <footer>Generated by @atlisp/lint on ${new Date().toISOString()}</footer>
84
+ </div>
85
+ </body>
86
+ </html>`;
87
+ }
88
+ function escHtml(s) {
89
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
90
+ }
91
+ //# sourceMappingURL=formatters-html.js.map
@@ -0,0 +1,3 @@
1
+ import { Issue } from './types';
2
+ export declare function formatSarif(issues: Issue[], errorCount: number, warningCount: number): string;
3
+ //# sourceMappingURL=formatters-sarif.d.ts.map