@atlisp/lint 0.1.5 → 0.1.8
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/LICENSE +21 -0
- package/README.md +219 -58
- package/atlisp-lint.default.json +32 -1
- package/dist/atlisp-lint.default.json +90 -0
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +6 -6
- package/dist/checks/append-single.d.ts +3 -0
- package/dist/checks/append-single.js +17 -0
- package/dist/checks/arg-count.d.ts +3 -0
- package/dist/checks/arg-count.js +123 -0
- package/dist/checks/assoc-without-cdr.d.ts +5 -0
- package/dist/checks/assoc-without-cdr.js +32 -0
- package/dist/checks/comment-style.d.ts +3 -0
- package/dist/checks/comment-style.js +24 -0
- package/dist/checks/cond-duplicate.d.ts +5 -0
- package/dist/checks/cond-duplicate.js +52 -0
- package/dist/checks/cond-simplify.d.ts +3 -0
- package/dist/checks/cond-simplify.js +45 -0
- package/dist/checks/constant-condition.js +4 -4
- package/dist/checks/dangerous-calls.js +2 -2
- package/dist/checks/dangling-defun.d.ts +3 -0
- package/dist/checks/dangling-defun.js +10 -28
- package/dist/checks/double-not.js +1 -1
- package/dist/checks/duplicate-defun.d.ts +6 -0
- package/dist/checks/duplicate-defun.js +50 -0
- package/dist/checks/dynamic-doc.d.ts +3 -0
- package/dist/checks/dynamic-doc.js +21 -0
- package/dist/checks/empty-branch.js +1 -1
- package/dist/checks/empty-catch.d.ts +3 -0
- package/dist/checks/empty-catch.js +34 -0
- package/dist/checks/eq-usage.d.ts +3 -0
- package/dist/checks/eq-usage.js +25 -0
- package/dist/checks/error-handling.d.ts +3 -0
- package/dist/checks/error-handling.js +56 -0
- package/dist/checks/extra-parens.d.ts +3 -0
- package/dist/checks/extra-parens.js +45 -0
- package/dist/checks/format-indent.d.ts +3 -0
- package/dist/checks/format-indent.js +29 -0
- package/dist/checks/function-complexity.js +1 -1
- package/dist/checks/function-order.d.ts +3 -0
- package/dist/checks/function-order.js +33 -0
- package/dist/checks/global-naming.d.ts +3 -0
- package/dist/checks/global-naming.js +62 -0
- package/dist/checks/identical-branches.d.ts +5 -0
- package/dist/checks/identical-branches.js +54 -0
- package/dist/checks/index.d.ts +3 -0
- package/dist/checks/index.js +117 -0
- package/dist/checks/lambda-syntax.d.ts +3 -0
- package/dist/checks/lambda-syntax.js +25 -0
- package/dist/checks/long-function-call.d.ts +3 -0
- package/dist/checks/long-function-call.js +54 -0
- package/dist/checks/loop-optimization.d.ts +3 -0
- package/dist/checks/loop-optimization.js +17 -0
- package/dist/checks/magic-number.d.ts +3 -0
- package/dist/checks/magic-number.js +21 -0
- package/dist/checks/misplaced-else.js +1 -1
- package/dist/checks/missing-export.js +2 -2
- package/dist/checks/mixed-indent.d.ts +3 -0
- package/dist/checks/mixed-indent.js +19 -0
- package/dist/checks/module-reg.js +1 -1
- package/dist/checks/multiple-setq.js +1 -1
- package/dist/checks/no-return.d.ts +3 -0
- package/dist/checks/no-return.js +51 -0
- package/dist/checks/nth-usage.d.ts +3 -0
- package/dist/checks/nth-usage.js +17 -0
- package/dist/checks/quote-style.d.ts +3 -0
- package/dist/checks/quote-style.js +25 -0
- package/dist/checks/quote-vs-function.js +1 -1
- package/dist/checks/recursive-call.js +1 -1
- package/dist/checks/redundant-if.d.ts +3 -0
- package/dist/checks/redundant-if.js +17 -0
- package/dist/checks/redundant-let.js +1 -1
- package/dist/checks/redundant-nil-else.js +1 -1
- package/dist/checks/redundant-progn.js +1 -1
- package/dist/checks/redundant-quotes.js +1 -1
- package/dist/checks/redundant-setq.js +1 -1
- package/dist/checks/self-compare.js +1 -1
- package/dist/checks/setq-multiple.d.ts +3 -0
- package/dist/checks/setq-multiple.js +17 -0
- package/dist/checks/setq-single-arg.d.ts +5 -0
- package/dist/checks/setq-single-arg.js +30 -0
- package/dist/checks/shadow-builtin.d.ts +3 -0
- package/dist/checks/shadow-builtin.js +77 -0
- package/dist/checks/single-arg-and-or.js +1 -1
- package/dist/checks/strcat-usage.d.ts +3 -0
- package/dist/checks/strcat-usage.js +25 -0
- package/dist/checks/type-check.d.ts +3 -0
- package/dist/checks/type-check.js +26 -0
- package/dist/checks/unused-let.js +1 -1
- package/dist/checks/unused-package-dep.js +3 -3
- package/dist/checks/variable-shadow.js +1 -1
- package/dist/checks/while-constant.d.ts +5 -0
- package/dist/checks/while-constant.js +40 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +71 -2
- package/dist/disable.js +1 -1
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +51 -0
- package/dist/formatters.d.ts +1 -0
- package/dist/formatters.js +18 -2
- package/dist/index.js +172 -32
- package/dist/lib/lint-sbcl.lisp +161 -0
- package/dist/locale.js +76 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +159 -0
- package/dist/project.js +37 -6
- package/dist/rules.d.ts +9 -0
- package/dist/rules.js +239 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +329 -12
- package/dist/sbcl.js +1 -1
- package/dist/stub-packages.json +41 -0
- package/dist/types.d.ts +6 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +126 -0
- package/dist/watch.d.ts +9 -0
- package/dist/watch.js +113 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -34,10 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadConfig = loadConfig;
|
|
37
|
+
exports.getCheckSeverity = getCheckSeverity;
|
|
37
38
|
exports.findOverrides = findOverrides;
|
|
38
39
|
exports.mergeOverrides = mergeOverrides;
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const presets_1 = require("./presets");
|
|
41
43
|
const DEFAULT_CONFIG = {
|
|
42
44
|
locale: 'zh',
|
|
43
45
|
source: {
|
|
@@ -56,14 +58,70 @@ const DEFAULT_CONFIG = {
|
|
|
56
58
|
token_in_url: 'warn',
|
|
57
59
|
open_without_close: 'warn',
|
|
58
60
|
bare_function_names: 'warn',
|
|
59
|
-
trailing_whitespace: 'warn',
|
|
60
61
|
line_length: 'warn',
|
|
61
62
|
function_complexity: 'warn',
|
|
62
63
|
parameter_naming: 'warn',
|
|
63
64
|
unused_variable: 'warn',
|
|
64
65
|
missing_doc: 'warn',
|
|
66
|
+
trailing_whitespace: 'warn',
|
|
65
67
|
module_registration: 'off',
|
|
66
68
|
namespace_header: 'off',
|
|
69
|
+
unused_parameter: 'warn',
|
|
70
|
+
constant_condition: 'warn',
|
|
71
|
+
redundant_progn: 'warn',
|
|
72
|
+
empty_branch: 'warn',
|
|
73
|
+
unused_let_binding: 'warn',
|
|
74
|
+
recursive_call: 'warn',
|
|
75
|
+
variable_shadow: 'warn',
|
|
76
|
+
redundant_cond: 'warn',
|
|
77
|
+
unused_local_fun: 'warn',
|
|
78
|
+
multiple_setq: 'warn',
|
|
79
|
+
redundant_quotes: 'warn',
|
|
80
|
+
trailing_paren: 'warn',
|
|
81
|
+
empty_comment: 'warn',
|
|
82
|
+
redundant_setq: 'warn',
|
|
83
|
+
redundant_nil_else: 'warn',
|
|
84
|
+
single_arg_and_or: 'warn',
|
|
85
|
+
redundant_let: 'warn',
|
|
86
|
+
self_compare: 'warn',
|
|
87
|
+
misplaced_else: 'warn',
|
|
88
|
+
quote_vs_function: 'warn',
|
|
89
|
+
commented_code: 'warn',
|
|
90
|
+
double_not: 'warn',
|
|
91
|
+
setq_single_arg: 'warn',
|
|
92
|
+
assoc_without_cdr: 'warn',
|
|
93
|
+
identical_branches: 'warn',
|
|
94
|
+
while_constant: 'warn',
|
|
95
|
+
cond_duplicate: 'warn',
|
|
96
|
+
duplicate_defun: 'warn',
|
|
97
|
+
dangling_defun: 'warn',
|
|
98
|
+
missing_export: 'warn',
|
|
99
|
+
unused_package_dep: 'warn',
|
|
100
|
+
error_handling: 'warn',
|
|
101
|
+
global_naming: 'warn',
|
|
102
|
+
extra_parens: 'warn',
|
|
103
|
+
arg_count: 'warn',
|
|
104
|
+
strcat_usage: 'warn',
|
|
105
|
+
cond_simplify: 'warn',
|
|
106
|
+
quote_style: 'warn',
|
|
107
|
+
eq_usage: 'warn',
|
|
108
|
+
lambda_syntax: 'warn',
|
|
109
|
+
comment_style: 'warn',
|
|
110
|
+
empty_catch: 'warn',
|
|
111
|
+
nth_usage: 'warn',
|
|
112
|
+
append_single: 'warn',
|
|
113
|
+
setq_multiple: 'warn',
|
|
114
|
+
function_order: 'off',
|
|
115
|
+
magic_number: 'off',
|
|
116
|
+
mixed_indent: 'warn',
|
|
117
|
+
long_function_call: 'warn',
|
|
118
|
+
no_return: 'warn',
|
|
119
|
+
shadow_builtin: 'warn',
|
|
120
|
+
dynamic_doc: 'warn',
|
|
121
|
+
loop_optimization: 'off',
|
|
122
|
+
type_check: 'off',
|
|
123
|
+
format_indent: 'off',
|
|
124
|
+
redundant_if: 'warn',
|
|
67
125
|
},
|
|
68
126
|
line_length: {
|
|
69
127
|
max: 100,
|
|
@@ -111,9 +169,17 @@ function loadConfig(configPath) {
|
|
|
111
169
|
}
|
|
112
170
|
const raw = fs.readFileSync(absPath, 'utf-8');
|
|
113
171
|
const user = JSON.parse(raw);
|
|
114
|
-
//
|
|
172
|
+
// Start from default config
|
|
115
173
|
const merged = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
174
|
+
// Apply preset if specified (preset applies before user overrides)
|
|
175
|
+
const preset = user.preset;
|
|
176
|
+
if (preset && presets_1.PRESETS[preset]) {
|
|
177
|
+
deepMerge(merged, presets_1.PRESETS[preset]);
|
|
178
|
+
}
|
|
179
|
+
// Apply user overrides (user settings take precedence)
|
|
116
180
|
deepMerge(merged, user);
|
|
181
|
+
// Remove non-config keys
|
|
182
|
+
delete merged.preset;
|
|
117
183
|
return merged;
|
|
118
184
|
}
|
|
119
185
|
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
@@ -133,6 +199,9 @@ function deepMerge(target, source) {
|
|
|
133
199
|
}
|
|
134
200
|
}
|
|
135
201
|
}
|
|
202
|
+
function getCheckSeverity(config, rule) {
|
|
203
|
+
return config.checks[rule] || 'off';
|
|
204
|
+
}
|
|
136
205
|
function findOverrides(filepath) {
|
|
137
206
|
const dir = path.dirname(filepath);
|
|
138
207
|
const configPath = path.join(dir, '.atlisp-lint.json');
|
package/dist/disable.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCode = formatCode;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function v1Format(content, indentSize) {
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
const result = [];
|
|
8
|
+
let depth = 0;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
const trimmedLine = line.trimEnd();
|
|
11
|
+
if (trimmedLine === '') {
|
|
12
|
+
result.push('');
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const stripped = (0, utils_1.stripLine)(trimmedLine);
|
|
16
|
+
const trimmedStripped = stripped.trim();
|
|
17
|
+
if (trimmedStripped === '') {
|
|
18
|
+
const indent = ' '.repeat(depth * indentSize);
|
|
19
|
+
result.push(indent + trimmedLine.trimStart());
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let openCount = 0;
|
|
23
|
+
let closeCount = 0;
|
|
24
|
+
for (const ch of trimmedStripped) {
|
|
25
|
+
if (ch === '(')
|
|
26
|
+
openCount++;
|
|
27
|
+
else if (ch === ')')
|
|
28
|
+
closeCount++;
|
|
29
|
+
}
|
|
30
|
+
let effectiveDepth = depth;
|
|
31
|
+
if (trimmedStripped.startsWith(')')) {
|
|
32
|
+
let leadingClose = 0;
|
|
33
|
+
for (const ch of trimmedStripped) {
|
|
34
|
+
if (ch === ')')
|
|
35
|
+
leadingClose++;
|
|
36
|
+
else
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
effectiveDepth = Math.max(0, depth - leadingClose);
|
|
40
|
+
}
|
|
41
|
+
result.push(' '.repeat(effectiveDepth * indentSize) + trimmedLine.trimStart());
|
|
42
|
+
depth += openCount - closeCount;
|
|
43
|
+
if (depth < 0)
|
|
44
|
+
depth = 0;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function formatCode(content, indentSize = 2) {
|
|
49
|
+
return v1Format(content, indentSize).join('\n');
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=formatter.js.map
|
package/dist/formatters.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Issue } from './types';
|
|
2
2
|
export declare function formatDefault(issues: Issue[], fileContents?: Map<string, string>): string;
|
|
3
|
+
export declare function formatHtml(issues: Issue[], errorCount: number, warningCount: number): string;
|
|
3
4
|
export declare function formatJson(issues: Issue[], errorCount?: number, warningCount?: number): string;
|
|
4
5
|
//# sourceMappingURL=formatters.d.ts.map
|
package/dist/formatters.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatDefault = formatDefault;
|
|
4
|
+
exports.formatHtml = formatHtml;
|
|
4
5
|
exports.formatJson = formatJson;
|
|
5
6
|
const locale_1 = require("./locale");
|
|
6
7
|
const colors = {
|
|
@@ -68,11 +69,26 @@ function formatDefault(issues, fileContents) {
|
|
|
68
69
|
}
|
|
69
70
|
return lines.join('\n');
|
|
70
71
|
}
|
|
72
|
+
function formatHtml(issues, errorCount, warningCount) {
|
|
73
|
+
let html = `<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head><meta charset="UTF-8"><title>@atlisp/lint Report</title></head>
|
|
76
|
+
<body>
|
|
77
|
+
<h1>@atlisp/lint Report</h1>
|
|
78
|
+
<p>${errorCount} error(s), ${warningCount} warning(s)</p>
|
|
79
|
+
<ul>
|
|
80
|
+
`;
|
|
81
|
+
for (const iss of issues) {
|
|
82
|
+
html += ` <li>${iss.severity}: ${iss.file}:${iss.line} — ${iss.message}</li>\n`;
|
|
83
|
+
}
|
|
84
|
+
html += '</ul>\n</body>\n</html>';
|
|
85
|
+
return html;
|
|
86
|
+
}
|
|
71
87
|
function formatJson(issues, errorCount, warningCount) {
|
|
72
88
|
return JSON.stringify({
|
|
73
89
|
issues,
|
|
74
|
-
error_count: errorCount ?? issues.filter(
|
|
75
|
-
warning_count: warningCount ?? issues.filter(
|
|
90
|
+
error_count: errorCount ?? issues.filter(i => i.severity === 'error').length,
|
|
91
|
+
warning_count: warningCount ?? issues.filter(i => i.severity === 'warn').length,
|
|
76
92
|
}, null, 2);
|
|
77
93
|
}
|
|
78
94
|
//# sourceMappingURL=formatters.js.map
|
package/dist/index.js
CHANGED
|
@@ -42,8 +42,10 @@ const runner_1 = require("./runner");
|
|
|
42
42
|
const project_1 = require("./project");
|
|
43
43
|
const sbcl_1 = require("./sbcl");
|
|
44
44
|
const formatters_1 = require("./formatters");
|
|
45
|
+
const formatter_1 = require("./formatter");
|
|
45
46
|
const locale_1 = require("./locale");
|
|
46
47
|
const cache_1 = require("./cache");
|
|
48
|
+
const watch_1 = require("./watch");
|
|
47
49
|
function parseArgs() {
|
|
48
50
|
const argv = process.argv.slice(2);
|
|
49
51
|
const opts = {
|
|
@@ -59,6 +61,12 @@ function parseArgs() {
|
|
|
59
61
|
clearCache: false,
|
|
60
62
|
parallel: false,
|
|
61
63
|
project: false,
|
|
64
|
+
watch: false,
|
|
65
|
+
help: false,
|
|
66
|
+
version: false,
|
|
67
|
+
formatCode: false,
|
|
68
|
+
formatCheck: false,
|
|
69
|
+
sbcl: false,
|
|
62
70
|
};
|
|
63
71
|
for (let i = 0; i < argv.length; i++) {
|
|
64
72
|
switch (argv[i]) {
|
|
@@ -101,6 +109,24 @@ function parseArgs() {
|
|
|
101
109
|
case '--project':
|
|
102
110
|
opts.project = true;
|
|
103
111
|
break;
|
|
112
|
+
case '--watch':
|
|
113
|
+
opts.watch = true;
|
|
114
|
+
break;
|
|
115
|
+
case '--help':
|
|
116
|
+
opts.help = true;
|
|
117
|
+
break;
|
|
118
|
+
case '--version':
|
|
119
|
+
opts.version = true;
|
|
120
|
+
break;
|
|
121
|
+
case '--format-code':
|
|
122
|
+
opts.formatCode = true;
|
|
123
|
+
break;
|
|
124
|
+
case '--format-check':
|
|
125
|
+
opts.formatCheck = true;
|
|
126
|
+
break;
|
|
127
|
+
case '--sbcl':
|
|
128
|
+
opts.sbcl = true;
|
|
129
|
+
break;
|
|
104
130
|
default:
|
|
105
131
|
break;
|
|
106
132
|
}
|
|
@@ -168,12 +194,12 @@ function collectFiles(rootDir, opts, config) {
|
|
|
168
194
|
}
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
|
-
const excludePatterns = config.source.exclude.map(
|
|
197
|
+
const excludePatterns = config.source.exclude.map(e => {
|
|
172
198
|
const str = e.replace(/\./g, '\\.').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
|
173
199
|
return new RegExp(`^${str}$`);
|
|
174
200
|
});
|
|
175
201
|
return Array.from(files)
|
|
176
|
-
.filter(
|
|
202
|
+
.filter(f => {
|
|
177
203
|
const rel = path.relative(rootDir, f).replace(/\\/g, '/');
|
|
178
204
|
for (const re of excludePatterns) {
|
|
179
205
|
if (re.test(rel))
|
|
@@ -190,10 +216,10 @@ function collectStagedFiles(rootDir) {
|
|
|
190
216
|
});
|
|
191
217
|
return output
|
|
192
218
|
.split('\n')
|
|
193
|
-
.map(
|
|
194
|
-
.filter(
|
|
195
|
-
.map(
|
|
196
|
-
.filter(
|
|
219
|
+
.map(l => l.trim())
|
|
220
|
+
.filter(l => l.endsWith('.lsp'))
|
|
221
|
+
.map(l => path.resolve(rootDir, l))
|
|
222
|
+
.filter(f => fs.existsSync(f));
|
|
197
223
|
}
|
|
198
224
|
function initConfig(rootDir) {
|
|
199
225
|
const defaultPath = path.join(__dirname, '..', 'atlisp-lint.default.json');
|
|
@@ -222,10 +248,27 @@ npx @atlisp/lint ${args} || exit 1
|
|
|
222
248
|
fs.chmodSync(hookPath, 0o755);
|
|
223
249
|
console.log((0, locale_1.t)('index.hook_installed', hookPath));
|
|
224
250
|
}
|
|
251
|
+
function printHelp() {
|
|
252
|
+
console.log((0, locale_1.t)('help.text'));
|
|
253
|
+
}
|
|
225
254
|
async function main() {
|
|
226
255
|
try {
|
|
227
256
|
const opts = parseArgs();
|
|
228
257
|
const rootDir = process.cwd();
|
|
258
|
+
// Load config early so locale is available for --help and other commands
|
|
259
|
+
const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
|
|
260
|
+
const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
|
|
261
|
+
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
262
|
+
if (opts.help) {
|
|
263
|
+
printHelp();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (opts.version) {
|
|
267
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
268
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
269
|
+
console.log(pkg.version);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
229
272
|
if (opts.init) {
|
|
230
273
|
initConfig(rootDir);
|
|
231
274
|
return;
|
|
@@ -239,16 +282,45 @@ async function main() {
|
|
|
239
282
|
console.log((0, locale_1.t)('index.cache_cleared'));
|
|
240
283
|
return;
|
|
241
284
|
}
|
|
242
|
-
// Load config
|
|
243
|
-
const configPath = opts.config || path.join(rootDir, 'atlisp-lint.json');
|
|
244
|
-
const config = (0, config_1.loadConfig)(fs.existsSync(configPath) ? configPath : undefined);
|
|
245
|
-
(0, locale_1.setLocale)(config.locale || 'zh');
|
|
246
285
|
// Collect files
|
|
247
286
|
const files = opts.staged ? collectStagedFiles(rootDir) : collectFiles(rootDir, opts, config);
|
|
248
287
|
if (files.length === 0) {
|
|
249
288
|
console.log((0, locale_1.t)('index.no_files'));
|
|
250
289
|
return;
|
|
251
290
|
}
|
|
291
|
+
// --format-code mode: format files in place
|
|
292
|
+
if (opts.formatCode || opts.formatCheck) {
|
|
293
|
+
let needsFormat = false;
|
|
294
|
+
for (const f of files) {
|
|
295
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
296
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
297
|
+
if (content !== formatted) {
|
|
298
|
+
if (opts.formatCheck) {
|
|
299
|
+
console.log(`${path.relative(rootDir, f)} — needs formatting`);
|
|
300
|
+
needsFormat = true;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
304
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', 'format')}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Idempotency check: format again to verify stability
|
|
308
|
+
if (!opts.formatCheck) {
|
|
309
|
+
const reFormatted = (0, formatter_1.formatCode)(fs.readFileSync(f, 'utf-8'));
|
|
310
|
+
if (reFormatted !== fs.readFileSync(f, 'utf-8')) {
|
|
311
|
+
console.log(`${(0, locale_1.t)('summary.tag_warn')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.format_unstable')}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (opts.formatCheck && needsFormat)
|
|
316
|
+
process.exit(1);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// --watch mode
|
|
320
|
+
if (opts.watch) {
|
|
321
|
+
(0, watch_1.watchFiles)(files, { rootDir, format: opts.format, config });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
252
324
|
// --fix mode: auto-fix before linting
|
|
253
325
|
if (opts.fix) {
|
|
254
326
|
for (const f of files) {
|
|
@@ -257,13 +329,39 @@ async function main() {
|
|
|
257
329
|
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', rule)}`);
|
|
258
330
|
}
|
|
259
331
|
}
|
|
332
|
+
// format_indent fix for non-staged mode (no lint results available yet)
|
|
333
|
+
if (config.checks['format_indent'] !== 'off') {
|
|
334
|
+
for (const f of files) {
|
|
335
|
+
try {
|
|
336
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
337
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
338
|
+
if (formatted !== content) {
|
|
339
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
340
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* ignore */
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
260
348
|
if (!opts.staged)
|
|
261
349
|
return;
|
|
262
350
|
}
|
|
263
|
-
// Filter cached files
|
|
351
|
+
// Filter cached files (pre-read content to avoid double I/O)
|
|
352
|
+
const contentCache = new Map();
|
|
264
353
|
const filesToLint = opts.cache
|
|
265
|
-
? files.filter(
|
|
266
|
-
|
|
354
|
+
? files.filter(f => {
|
|
355
|
+
let content;
|
|
356
|
+
try {
|
|
357
|
+
content = fs.readFileSync(f, 'utf-8');
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
/* ignore */
|
|
361
|
+
}
|
|
362
|
+
if (content !== undefined)
|
|
363
|
+
contentCache.set(f, content);
|
|
364
|
+
const cached = (0, cache_1.isCached)(f, rootDir, content);
|
|
267
365
|
if (cached) {
|
|
268
366
|
console.log(`${(0, locale_1.t)('summary.tag_warn')}: ${path.relative(rootDir, f)} — ${(0, locale_1.t)('index.cached')}`);
|
|
269
367
|
}
|
|
@@ -283,27 +381,69 @@ async function main() {
|
|
|
283
381
|
const projectIssues = (0, project_1.lintProject)(filesToLint, config, rootDir);
|
|
284
382
|
issues.push(...projectIssues);
|
|
285
383
|
}
|
|
286
|
-
//
|
|
287
|
-
if (opts.
|
|
384
|
+
// Apply rule-based fixes (after lint, when --fix is set)
|
|
385
|
+
if (opts.fix) {
|
|
288
386
|
for (const f of filesToLint) {
|
|
289
|
-
|
|
387
|
+
const relPath = path.relative(rootDir, f);
|
|
388
|
+
const fileIssues = issues.filter(i => i.file === relPath);
|
|
389
|
+
if (fileIssues.length > 0) {
|
|
390
|
+
try {
|
|
391
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
392
|
+
const fixed = (0, runner_1.applyFixes)(fileIssues, content, relPath);
|
|
393
|
+
if (fixed !== content) {
|
|
394
|
+
fs.writeFileSync(f, fixed, 'utf-8');
|
|
395
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed')}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// format_indent fix: apply formatCode to files with format_indent issues
|
|
404
|
+
if (config.checks['format_indent'] !== 'off') {
|
|
405
|
+
for (const f of filesToLint) {
|
|
406
|
+
const relPath = path.relative(rootDir, f);
|
|
407
|
+
const hasFormatIssue = issues.some(i => i.file === relPath && i.rule === 'format_indent');
|
|
408
|
+
if (hasFormatIssue) {
|
|
409
|
+
try {
|
|
410
|
+
const content = fs.readFileSync(f, 'utf-8');
|
|
411
|
+
const formatted = (0, formatter_1.formatCode)(content);
|
|
412
|
+
if (formatted !== content) {
|
|
413
|
+
fs.writeFileSync(f, formatted, 'utf-8');
|
|
414
|
+
console.log(`${(0, locale_1.t)('summary.tag_fail')}: ${relPath} — ${(0, locale_1.t)('index.fixed', 'format_indent')}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
/* ignore */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
290
422
|
}
|
|
291
423
|
}
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
424
|
+
// Mark as cached (reuse pre-read content if available)
|
|
425
|
+
if (opts.cache) {
|
|
426
|
+
for (const f of filesToLint) {
|
|
427
|
+
(0, cache_1.markCached)(f, rootDir, contentCache.get(f));
|
|
428
|
+
}
|
|
297
429
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
430
|
+
// Phase 2: SBCL syntax validation (only when --sbcl is specified)
|
|
431
|
+
if (opts.sbcl) {
|
|
432
|
+
const stubPkgPath = path.join(__dirname, '..', 'stub-packages.json');
|
|
433
|
+
try {
|
|
434
|
+
const sbclIssues = (0, sbcl_1.runSbclLint)(rootDir, config.sbcl, stubPkgPath);
|
|
435
|
+
issues.push(...sbclIssues);
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
439
|
+
issues.push({
|
|
440
|
+
file: 'sbcl',
|
|
441
|
+
line: 1,
|
|
442
|
+
severity: 'error',
|
|
443
|
+
rule: 'sbcl',
|
|
444
|
+
message: msg,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
307
447
|
}
|
|
308
448
|
// Build file content map for code line display
|
|
309
449
|
const fileContents = new Map();
|
|
@@ -319,8 +459,8 @@ async function main() {
|
|
|
319
459
|
}
|
|
320
460
|
}
|
|
321
461
|
// Format & output
|
|
322
|
-
const errorCount = issues.filter(
|
|
323
|
-
const warningCount = issues.filter(
|
|
462
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
463
|
+
const warningCount = issues.filter(i => i.severity === 'warn').length;
|
|
324
464
|
if (opts.format === 'json') {
|
|
325
465
|
console.log((0, formatters_1.formatJson)(issues, errorCount, warningCount));
|
|
326
466
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
;; @lisp SBCL Lint — syntax validation via SBCL
|
|
2
|
+
;; Usage: sbcl --script lint-sbcl.lisp <src-dir> <stub-packages.json> <walk-exclude-json> <defmacro-allow-files-json> <locale>
|
|
3
|
+
|
|
4
|
+
(require :uiop)
|
|
5
|
+
|
|
6
|
+
(defparameter *errors* 0)
|
|
7
|
+
(defparameter *warnings* 0)
|
|
8
|
+
(defparameter *checked* 0)
|
|
9
|
+
(defparameter *locale* "zh")
|
|
10
|
+
|
|
11
|
+
;; ── i18n ──────────────────────────────────────────────────────────────
|
|
12
|
+
(defun i18n (key)
|
|
13
|
+
(cdr (assoc key
|
|
14
|
+
(if (string= *locale* "zh")
|
|
15
|
+
'((:not-found . "not found symbol")
|
|
16
|
+
(:syntax-error . "syntax error")
|
|
17
|
+
(:vec-syntax . "#( vector syntax")
|
|
18
|
+
(:char-syntax . "# backslash char literal")
|
|
19
|
+
(:eval-syntax . "#. read-time eval")
|
|
20
|
+
(:defmacro . "defmacro")
|
|
21
|
+
(:trailing-ws . "trailing whitespace")
|
|
22
|
+
(:ok . "OK")
|
|
23
|
+
(:fail . "FAIL")
|
|
24
|
+
(:header . " @lisp SBCL Lint")
|
|
25
|
+
(:source . " Source")
|
|
26
|
+
(:found . " Found ~d .lsp files")
|
|
27
|
+
(:scanned . " Scanned")
|
|
28
|
+
(:errors . " Errors")
|
|
29
|
+
(:warnings . " Warnings"))
|
|
30
|
+
'((:not-found . "not found symbol")
|
|
31
|
+
(:syntax-error . "syntax error")
|
|
32
|
+
(:vec-syntax . "#( vector syntax")
|
|
33
|
+
(:char-syntax . "# backslash char literal")
|
|
34
|
+
(:eval-syntax . "#. read-time eval")
|
|
35
|
+
(:defmacro . "defmacro")
|
|
36
|
+
(:trailing-ws . "trailing whitespace")
|
|
37
|
+
(:ok . "OK")
|
|
38
|
+
(:fail . "FAIL")
|
|
39
|
+
(:header . " @lisp SBCL Lint")
|
|
40
|
+
(:source . " Source")
|
|
41
|
+
(:found . " Found ~d .lsp files")
|
|
42
|
+
(:scanned . " Scanned")
|
|
43
|
+
(:errors . " Errors")
|
|
44
|
+
(:warnings . " Warnings"))))))
|
|
45
|
+
|
|
46
|
+
;; ── Stub packages from external file ──────────────────────────────────
|
|
47
|
+
(defun load-stub-packages (file-path)
|
|
48
|
+
(flet ((ensure-pkg (name &optional use-list)
|
|
49
|
+
(unless (find-package name)
|
|
50
|
+
(make-package name :use (or use-list '())))))
|
|
51
|
+
(with-open-file (s file-path :direction :input)
|
|
52
|
+
(let ((packages (read s)))
|
|
53
|
+
(dolist (pkg packages)
|
|
54
|
+
(ensure-pkg (first pkg) (second pkg)))))))
|
|
55
|
+
|
|
56
|
+
;; ── File walking ──────────────────────────────────────────────────────
|
|
57
|
+
(defun source-files (dir)
|
|
58
|
+
(sort
|
|
59
|
+
(remove-if-not
|
|
60
|
+
(lambda (p)
|
|
61
|
+
(let ((n (pathname-type p)))
|
|
62
|
+
(and n (string-equal n "lsp"))))
|
|
63
|
+
(uiop:directory-files dir))
|
|
64
|
+
'string< :key #'namestring))
|
|
65
|
+
|
|
66
|
+
(defun walk-tree (dir exclude-dirs)
|
|
67
|
+
(let ((result (source-files dir)))
|
|
68
|
+
(dolist (sub (uiop:subdirectories dir))
|
|
69
|
+
(let ((name (car (last (pathname-directory sub)))))
|
|
70
|
+
(unless (find name exclude-dirs :test 'string=)
|
|
71
|
+
(setf result (append result (walk-tree sub exclude-dirs))))))
|
|
72
|
+
result))
|
|
73
|
+
|
|
74
|
+
;; ── Per-file check ────────────────────────────────────────────────────
|
|
75
|
+
(defun check-file (path src-dir defmacro-allow-files)
|
|
76
|
+
(incf *checked*)
|
|
77
|
+
(let* ((rel (enough-namestring path src-dir))
|
|
78
|
+
(fullname (format nil "~a.~a" (pathname-name path) (pathname-type path))))
|
|
79
|
+
(format t " ~a ... " rel)
|
|
80
|
+
(force-output)
|
|
81
|
+
;; Syntax check via READ
|
|
82
|
+
(handler-case
|
|
83
|
+
(with-open-file (s path :direction :input)
|
|
84
|
+
(loop for form = (read s nil :eof)
|
|
85
|
+
until (eq form :eof)))
|
|
86
|
+
(error (e)
|
|
87
|
+
(let ((msg (princ-to-string e)))
|
|
88
|
+
(cond
|
|
89
|
+
((search "not found" msg)
|
|
90
|
+
(format t "~% [NOTE] ~a: ~a~%" rel (i18n :not-found))
|
|
91
|
+
(incf *warnings*))
|
|
92
|
+
(t
|
|
93
|
+
(format t "~a~% [ERROR] ~a: ~a~%" (i18n :fail) rel (i18n :syntax-error))
|
|
94
|
+
(incf *errors*)
|
|
95
|
+
(return-from check-file))))))
|
|
96
|
+
;; Trailing whitespace
|
|
97
|
+
(with-open-file (s path :direction :input)
|
|
98
|
+
(loop for line = (read-line s nil nil)
|
|
99
|
+
for lineno from 1
|
|
100
|
+
while line
|
|
101
|
+
when (and (> (length line) 0)
|
|
102
|
+
(find (char line (1- (length line))) '(#\Space #\Tab)))
|
|
103
|
+
do (progn
|
|
104
|
+
(format t "~% [WARN] ~a line ~d: ~a~%" rel lineno (i18n :trailing-ws))
|
|
105
|
+
(incf *warnings*))))
|
|
106
|
+
;; CL-ism checks: raw content scan
|
|
107
|
+
(with-open-file (s path :direction :input)
|
|
108
|
+
(let ((content (make-string (file-length s))))
|
|
109
|
+
(file-position s 0)
|
|
110
|
+
(read-sequence content s)
|
|
111
|
+
(when (search "#(" content)
|
|
112
|
+
(format t "~% [WARN] ~a: ~a~%" rel (i18n :vec-syntax))
|
|
113
|
+
(incf *warnings*))
|
|
114
|
+
(when (search "#\\" content)
|
|
115
|
+
(format t "~% [WARN] ~a: ~a~%" rel (i18n :char-syntax))
|
|
116
|
+
(incf *warnings*))
|
|
117
|
+
(when (search "#." content)
|
|
118
|
+
(format t "~% [WARN] ~a: ~a~%" rel (i18n :eval-syntax))
|
|
119
|
+
(incf *warnings*))
|
|
120
|
+
(when (and (search "defmacro" content)
|
|
121
|
+
(not (some (lambda (s) (search s fullname)) defmacro-allow-files)))
|
|
122
|
+
(format t "~% [WARN] ~a: ~a~%" rel (i18n :defmacro))
|
|
123
|
+
(incf *warnings*))))
|
|
124
|
+
(format t "~a~%" (i18n :ok))))
|
|
125
|
+
|
|
126
|
+
;; ── Main ──────────────────────────────────────────────────────────────
|
|
127
|
+
(defun main ()
|
|
128
|
+
(let* ((args (uiop:command-line-arguments))
|
|
129
|
+
(src-dir (first args))
|
|
130
|
+
(stub-file (second args))
|
|
131
|
+
(walk-exclude (if (third args)
|
|
132
|
+
(read-from-string (third args))
|
|
133
|
+
'(".vscode" "test" "experiment" "tools" ".git")))
|
|
134
|
+
(defmacro-allow (if (fourth args)
|
|
135
|
+
(read-from-string (fourth args))
|
|
136
|
+
'("compat-cl")))
|
|
137
|
+
(locale (fifth args)))
|
|
138
|
+
(when locale (setf *locale* locale))
|
|
139
|
+
(load-stub-packages stub-file)
|
|
140
|
+
|
|
141
|
+
(format t "~%==================================================~%")
|
|
142
|
+
(format t "~a~%" (i18n :header))
|
|
143
|
+
(format t "~a: ~a~%" (i18n :source) (namestring (pathname src-dir)))
|
|
144
|
+
(format t "==================================================~%~%")
|
|
145
|
+
(setf *errors* 0 *warnings* 0 *checked* 0)
|
|
146
|
+
|
|
147
|
+
(let ((files (walk-tree src-dir walk-exclude)))
|
|
148
|
+
(format t "~a~%~%" (format nil (i18n :found) (length files)))
|
|
149
|
+
(dolist (f files)
|
|
150
|
+
(check-file f src-dir defmacro-allow)))
|
|
151
|
+
|
|
152
|
+
(format t "~%==================================================~%")
|
|
153
|
+
(format t "~a: ~d~%" (i18n :scanned) *checked*)
|
|
154
|
+
(format t "~a: ~d~%" (i18n :errors) *errors*)
|
|
155
|
+
(format t "~a: ~d~%" (i18n :warnings) *warnings*)
|
|
156
|
+
(format t "==================================================~%~%")
|
|
157
|
+
(if (> *errors* 0)
|
|
158
|
+
(uiop:quit 1)
|
|
159
|
+
(uiop:quit 0))))
|
|
160
|
+
|
|
161
|
+
(main)
|