@atlisp/lint 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -50,8 +50,11 @@ npx @atlisp/lint --format-check
50
50
  --clear-cache 清除缓存目录
51
51
  --parallel 开启多线程并行检查(使用 Worker Threads)
52
52
  --project 启用跨文件分析(Phase 1b)
53
+ --sbcl 启用 SBCL 语法校验(Phase 2,需安装 SBCL)
53
54
  --watch 监听模式,文件变动自动重新检查
54
55
  --hook-args <args> 自定义 pre-commit hook 参数(配合 --install-hook)
56
+ --diff 仅检查自上次提交后变更的文件
57
+ --changed <branch> 仅检查相对于某分支有变更的文件(默认:main)
55
58
  --help 显示帮助信息
56
59
  --version 显示版本号
57
60
  --format-code 自动格式化代码缩进(基于括号深度 + AST 对齐)
@@ -60,9 +63,9 @@ npx @atlisp/lint --format-check
60
63
 
61
64
  ## 架构:三相检测
62
65
 
63
- ### Phase 1a — 单文件静态检查(74 条规则)
66
+ ### Phase 1a — 单文件静态检查(84 条规则)
64
67
 
65
- 所有 74 条规则在单文件范围内运行,支持 `--parallel` 多线程并行。
68
+ 所有 84 条规则在单文件范围内运行,支持 `--parallel` 多线程并行。
66
69
 
67
70
  | 规则 | 默认 | 类别 | 说明 |
68
71
  |------|------|------|------|
@@ -136,6 +139,13 @@ npx @atlisp/lint --format-check
136
139
  | `namespace_header` | off | 架构 | 缺少 (in-package ...) 头 |
137
140
  | `loop_optimization` | off | 最佳实践 | 优化 while/assoc 循环 |
138
141
  | `type_check` | off | 最佳实践 | getvar 结果未类型检查 |
142
+ | `builtin_arg_count` | error | 正确性 | 内置函数调用参数个数不匹配 |
143
+ | `unused_catch_result` | info | 正确性 | vl-catch-all-apply 结果被当布尔值用 |
144
+ | `string_concat_loop` | info | 性能 | 循环内字符串拼接 |
145
+ | `if_without_else` | info | 风格 | if 无 else 分支 |
146
+ | `redundant_list` | info | 风格 | (list) 等价于 nil |
147
+ | `entget_in_loop` | warn | 性能 | 循环内 entget |
148
+ | `promise_handler` | info | 最佳实践 | vlax-invoke 缺少回调处理器 |
139
149
 
140
150
  ### Phase 1b — 跨文件分析(--project)
141
151
 
@@ -146,6 +156,8 @@ npx @atlisp/lint --format-check
146
156
  | `dangling_defun` | warn | 函数定义但从未被任何文件调用 |
147
157
  | `missing_export` | warn | 函数未注册到 @::\*modules\* |
148
158
  | `unused_package_dep` | warn | 导入的包从未被引用 |
159
+ | `module_cycle` | warn | 模块循环依赖 |
160
+ | `arg_count_project` | warn | 跨文件检查函数参数数量 |
149
161
 
150
162
  同时检测 `duplicate_defun`(函数名在多个文件中重复定义)。
151
163
 
@@ -71,7 +71,7 @@
71
71
  "setq_multiple": "warn",
72
72
  "format_indent": "off",
73
73
  "function_order": "off",
74
- "magic_number": "off",
74
+ "magic_number": "info",
75
75
  "mixed_indent": "warn",
76
76
  "long_function_call": "info",
77
77
  "no_return": "warn",
@@ -82,7 +82,7 @@
82
82
  "redundant_if": "warn",
83
83
  "undeclared_setq": "warn",
84
84
  "unused_catch_result": "off",
85
- "string_concat_loop": "off",
85
+ "string_concat_loop": "info",
86
86
  "if_without_else": "off",
87
87
  "redundant_list": "warn",
88
88
  "entget_in_loop": "warn",
@@ -35,7 +35,7 @@ const BUILTIN_ARITY = {
35
35
  minusp: { min: 1, max: 1 },
36
36
  plusp: { min: 1, max: 1 },
37
37
  eq: { min: 2, max: 2 },
38
- equal: { min: 2, max: 2 },
38
+ equal: { min: 2, max: 3 },
39
39
  '=': { min: 2, max: -1 },
40
40
  '/=': { min: 2, max: -1 },
41
41
  '<': { min: 2, max: -1 },
@@ -115,7 +115,7 @@ const BUILTIN_ARITY = {
115
115
  'read-char': { min: 0, max: 1 },
116
116
  'write-line': { min: 1, max: 2 },
117
117
  'write-char': { min: 1, max: 2 },
118
- 'read': { min: 0, max: 1 },
118
+ read: { min: 0, max: 1 },
119
119
  princ: { min: 0, max: 2 },
120
120
  prin1: { min: 0, max: 2 },
121
121
  print: { min: 0, max: 2 },
@@ -131,7 +131,7 @@ const BUILTIN_ARITY = {
131
131
  getorient: { min: 0, max: 3 },
132
132
  getkword: { min: 0, max: 2 },
133
133
  initget: { min: 1, max: 2 },
134
- 'load': { min: 1, max: 2 },
134
+ load: { min: 1, max: 2 },
135
135
  autoload: { min: 2, max: 2 },
136
136
  // === System ===
137
137
  getvar: { min: 1, max: 1 },
@@ -156,7 +156,7 @@ const BUILTIN_ARITY = {
156
156
  entnext: { min: 0, max: 1 },
157
157
  handent: { min: 1, max: 1 },
158
158
  entupd: { min: 1, max: 1 },
159
- 'ssget': { min: 0, max: -1 },
159
+ ssget: { min: 0, max: -1 },
160
160
  ssadd: { min: 0, max: 2 },
161
161
  ssdel: { min: 2, max: 2 },
162
162
  sslength: { min: 1, max: 1 },
@@ -66,7 +66,7 @@ function getSetqVarName(node) {
66
66
  return null;
67
67
  }
68
68
  function hasErrorCheckInSiblings(node, varName) {
69
- let parent = node.parent;
69
+ const parent = node.parent;
70
70
  if (!parent || !parent.children)
71
71
  return false;
72
72
  const idx = parent.children.indexOf(node);
@@ -88,7 +88,8 @@ function hasErrorCheckInSiblings(node, varName) {
88
88
  }
89
89
  function containsErrorCheck(node, varName) {
90
90
  const matches = (0, parser_1.astFindAll)(node, n => (0, parser_1.astIsList)(n, 'vl-catch-all-error-p') &&
91
- n.children && n.children.length >= 2 &&
91
+ n.children &&
92
+ n.children.length >= 2 &&
92
93
  (0, parser_1.astIsSymbol)(n.children[1], varName));
93
94
  return matches.length > 0;
94
95
  }
@@ -2,20 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkEntgetInLoop = checkEntgetInLoop;
4
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
5
  function checkEntgetInLoop(content, file) {
20
6
  const issues = [];
21
7
  const lines = content.split('\n');
@@ -11,7 +11,10 @@ function checkIfWithoutElse(ast, file) {
11
11
  continue;
12
12
  if (node.children.length === 3) {
13
13
  const parent = node.parent;
14
- if (parent && parent.type === 'list' && parent.children && parent.children.length > 0 &&
14
+ if (parent &&
15
+ parent.type === 'list' &&
16
+ parent.children &&
17
+ parent.children.length > 0 &&
15
18
  parent.children[0].type === 'symbol') {
16
19
  const parentName = parent.children[0].name;
17
20
  if (parentName === 'cond' || parentName === 'if')
@@ -12,7 +12,7 @@ function checkMagicNumber(content, file) {
12
12
  if (m) {
13
13
  const num = m[1];
14
14
  if (num !== '10' && num !== '20' && num !== '100' && num !== '1000') {
15
- issues.push({ file, line: i + 1, severity: 'warn', rule: 'magic_number', message: (0, locale_1.t)('magic_number', num) });
15
+ issues.push({ file, line: i + 1, severity: 'off', rule: 'magic_number', message: (0, locale_1.t)('magic_number', num) });
16
16
  }
17
17
  }
18
18
  }
@@ -5,21 +5,31 @@ const locale_1 = require("../locale");
5
5
  const parser_1 = require("@atlisp/parser");
6
6
  function checkPromiseHandler(ast, file) {
7
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'))));
8
+ const vlaxNodes = (0, parser_1.astFindAll)(ast, (n) => !!(n.type === 'list' &&
9
+ n.children &&
10
+ n.children.length > 0 &&
11
+ n.children[0].type === 'symbol' &&
12
+ typeof n.children[0].name === 'string' &&
13
+ (n.children[0].name.startsWith('vlax-invoke') ||
14
+ n.children[0].name.startsWith('vlax-invoke-method'))));
11
15
  for (const node of vlaxNodes) {
12
16
  if (isInQuote(node))
13
17
  continue;
14
18
  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') {
19
+ if (parent &&
20
+ parent.type === 'list' &&
21
+ parent.children &&
22
+ parent.children.length > 0 &&
23
+ parent.children[0].type === 'symbol' &&
24
+ parent.children[0].name === 'vl-catch-all-apply') {
17
25
  continue;
18
26
  }
19
27
  let hasCallback = false;
20
28
  if (node.children) {
21
29
  for (const child of node.children) {
22
- if (child.type === 'list' && child.children && child.children.length > 0 &&
30
+ if (child.type === 'list' &&
31
+ child.children &&
32
+ child.children.length > 0 &&
23
33
  child.children[0].type === 'symbol' &&
24
34
  (child.children[0].name === 'lambda' || child.children[0].name === 'function')) {
25
35
  hasCallback = true;
@@ -42,8 +52,11 @@ function checkPromiseHandler(ast, file) {
42
52
  function isInQuote(node) {
43
53
  let p = node.parent;
44
54
  while (p) {
45
- if (p.type === 'list' && p.children && p.children.length > 0 &&
46
- p.children[0].type === 'symbol' && p.children[0].name === 'quote') {
55
+ if (p.type === 'list' &&
56
+ p.children &&
57
+ p.children.length > 0 &&
58
+ p.children[0].type === 'symbol' &&
59
+ p.children[0].name === 'quote') {
47
60
  return true;
48
61
  }
49
62
  p = p.parent;
@@ -2,21 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkStringConcatInLoop = checkStringConcatInLoop;
4
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
5
  function checkStringConcatInLoop(content, file) {
21
6
  const issues = [];
22
7
  const lines = content.split('\n');
@@ -27,8 +12,6 @@ function checkStringConcatInLoop(content, file) {
27
12
  const name = match[1];
28
13
  let depth = 0;
29
14
  let started = false;
30
- let startLine = i;
31
- let foundStrcat = false;
32
15
  for (let j = i; j < lines.length; j++) {
33
16
  for (let k = 0; k < lines[j].length; k++) {
34
17
  const ch = lines[j][k];
@@ -47,7 +30,7 @@ function checkStringConcatInLoop(content, file) {
47
30
  issues.push({
48
31
  file,
49
32
  line: i + 1,
50
- severity: 'warn',
33
+ severity: 'info',
51
34
  rule: 'string_concat_loop',
52
35
  message: (0, locale_1.t)('string_concat_loop', name),
53
36
  });
package/dist/config.js CHANGED
@@ -113,7 +113,7 @@ const DEFAULT_CONFIG = {
113
113
  append_single: 'info',
114
114
  setq_multiple: 'warn',
115
115
  function_order: 'off',
116
- magic_number: 'info',
116
+ magic_number: 'off',
117
117
  mixed_indent: 'info',
118
118
  long_function_call: 'info',
119
119
  no_return: 'info',
@@ -55,11 +55,13 @@ function formatHtml(issues, errorCount, warningCount) {
55
55
  <p class="text-muted">${issues.length} total issues across ${ruleGroups.size} rules</p>
56
56
  </div>
57
57
 
58
- ${sortedRules.map(([rule, ruleIssues]) => `
58
+ ${sortedRules
59
+ .map(([rule, ruleIssues]) => `
59
60
  <div class="summary-card">
60
61
  <h5 class="card-title">${escHtml(rule)} <span class="badge bg-secondary">${ruleIssues.length}</span></h5>
61
62
  <ul class="list-group list-group-flush">
62
- ${ruleIssues.map(iss => `
63
+ ${ruleIssues
64
+ .map(iss => `
63
65
  <li class="list-group-item d-flex justify-content-between align-items-start">
64
66
  <div>
65
67
  <span class="fw-bold">${escHtml(iss.file)}:${iss.line || '-'}</span>
@@ -67,10 +69,12 @@ function formatHtml(issues, errorCount, warningCount) {
67
69
  </div>
68
70
  ${iss.severity === 'error' ? '<span class="badge bg-danger rounded-pill">ERROR</span>' : iss.severity === 'warn' ? '<span class="badge bg-warning text-dark rounded-pill">WARN</span>' : '<span class="badge bg-info text-dark rounded-pill">INFO</span>'}
69
71
  </li>
70
- `).join('')}
72
+ `)
73
+ .join('')}
71
74
  </ul>
72
75
  </div>
73
- `).join('')}
76
+ `)
77
+ .join('')}
74
78
 
75
79
  <div class="summary-card">
76
80
  <h5>All Issues</h5>
@@ -23,7 +23,11 @@ function formatDefault(issues, fileContents) {
23
23
  else if (iss.severity === 'warn')
24
24
  warnings++;
25
25
  const tagColor = iss.severity === 'error' ? colors.red : iss.severity === 'warn' ? colors.yellow : colors.cyan;
26
- const tag = iss.severity === 'error' ? (0, locale_1.t)('summary.tag_fail') : iss.severity === 'warn' ? (0, locale_1.t)('summary.tag_warn') : (0, locale_1.t)('summary.tag_info');
26
+ const tag = iss.severity === 'error'
27
+ ? (0, locale_1.t)('summary.tag_fail')
28
+ : iss.severity === 'warn'
29
+ ? (0, locale_1.t)('summary.tag_warn')
30
+ : (0, locale_1.t)('summary.tag_info');
27
31
  const loc = iss.line > 0 ? `${(0, locale_1.t)('summary.line')} ${iss.line}` : '';
28
32
  const prefix = useColor ? `${tagColor}${tag}${colors.reset}` : tag;
29
33
  lines.push(`${prefix}: ${iss.file} — ${loc}${loc ? ': ' : ''}${iss.message}`.trim());
package/dist/index.js CHANGED
@@ -363,7 +363,11 @@ async function main() {
363
363
  }
364
364
  // --watch mode
365
365
  if (opts.watch) {
366
- (0, watch_1.watchFiles)(files, { rootDir, format: (opts.format === 'sarif' || opts.format === 'html' ? 'default' : opts.format), config });
366
+ (0, watch_1.watchFiles)(files, {
367
+ rootDir,
368
+ format: (opts.format === 'sarif' || opts.format === 'html' ? 'default' : opts.format),
369
+ config,
370
+ });
367
371
  return;
368
372
  }
369
373
  // --fix mode: auto-fix before linting
package/dist/locale.js CHANGED
@@ -36,7 +36,7 @@ const messages = {
36
36
  unused_parameter: "Parameter '{0}' in '{1}' is never used",
37
37
  constant_condition: "Constant condition '{0}' in {1} — possibly leftover debug code",
38
38
  redundant_progn: 'Redundant progn in {0}: only one form inside',
39
- redundant_if: "Redundant progn in when/unless body — when/unless already allows multiple forms",
39
+ redundant_if: 'Redundant progn in when/unless body — when/unless already allows multiple forms',
40
40
  empty_branch: "Empty branch in '{0}' — no body forms",
41
41
  unused_let_binding: "Let binding '{0}' is never used",
42
42
  recursive_call: "Function '{0}' calls itself — possible infinite recursion",
package/dist/presets.js CHANGED
@@ -141,7 +141,7 @@ exports.PRESETS = {
141
141
  append_single: 'error',
142
142
  setq_multiple: 'error',
143
143
  function_order: 'off',
144
- magic_number: 'warn',
144
+ magic_number: 'off',
145
145
  mixed_indent: 'error',
146
146
  long_function_call: 'error',
147
147
  no_return: 'error',
@@ -154,7 +154,7 @@ exports.PRESETS = {
154
154
  module_registration: 'error',
155
155
  namespace_header: 'error',
156
156
  unused_catch_result: 'error',
157
- string_concat_loop: 'off',
157
+ string_concat_loop: 'info',
158
158
  if_without_else: 'off',
159
159
  redundant_list: 'error',
160
160
  entget_in_loop: 'warn',
package/dist/project.js CHANGED
@@ -139,9 +139,12 @@ function findModuleDeps(filepath) {
139
139
  }
140
140
  const setqNodes = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'setq'));
141
141
  for (const node of setqNodes) {
142
- if (node.children && node.children.length >= 3 &&
143
- node.children[1].type === 'symbol' && node.children[1].name === '@::*modules*' &&
144
- node.children[2].type === 'list' && node.children[2].children) {
142
+ if (node.children &&
143
+ node.children.length >= 3 &&
144
+ node.children[1].type === 'symbol' &&
145
+ node.children[1].name === '@::*modules*' &&
146
+ node.children[2].type === 'list' &&
147
+ node.children[2].children) {
145
148
  for (const exp of node.children[2].children) {
146
149
  if (exp.type === 'string' && typeof exp.value === 'string') {
147
150
  exports.push(exp.value);
@@ -235,7 +238,7 @@ function lintProject(files, config, rootDir) {
235
238
  const visited = new Set();
236
239
  const stack = new Set();
237
240
  const cyclePath = [];
238
- function dfs(current) {
241
+ const dfs = (current) => {
239
242
  if (stack.has(current)) {
240
243
  const idx = cyclePath.indexOf(current);
241
244
  const cycle = cyclePath.slice(idx).concat(current);
@@ -268,7 +271,7 @@ function lintProject(files, config, rootDir) {
268
271
  cyclePath.pop();
269
272
  stack.delete(current);
270
273
  return false;
271
- }
274
+ };
272
275
  for (const f of moduleDeps.keys()) {
273
276
  if (!visited.has(f)) {
274
277
  dfs(f);
package/dist/rules.js CHANGED
@@ -104,7 +104,7 @@ exports.RULES = [
104
104
  description: '检测可优化的 while/assoc 循环',
105
105
  category: '最佳实践',
106
106
  },
107
- { name: 'magic_number', defaultSeverity: 'info', description: '检测硬编码的魔法数字', category: '风格' },
107
+ { name: 'magic_number', defaultSeverity: 'off', description: '检测硬编码的魔法数字', category: '风格' },
108
108
  {
109
109
  name: 'misplaced_else',
110
110
  defaultSeverity: 'info',
@@ -156,7 +156,12 @@ exports.RULES = [
156
156
  description: '检测函数调用自身(可能无限递归)',
157
157
  category: '正确性',
158
158
  },
159
- { name: 'redundant_if', defaultSeverity: 'info', description: '检测 when/unless 中冗余 progn(when/unless 已隐含 progn)', category: '风格' },
159
+ {
160
+ name: 'redundant_if',
161
+ defaultSeverity: 'info',
162
+ description: '检测 when/unless 中冗余 progn(when/unless 已隐含 progn)',
163
+ category: '风格',
164
+ },
160
165
  { name: 'redundant_let', defaultSeverity: 'info', description: '检测无绑定的 let 建议改用 progn', category: '风格' },
161
166
  { name: 'redundant_nil_else', defaultSeverity: 'info', description: '检测 if 中冗余的 nil 分支', category: '风格' },
162
167
  { name: 'redundant_progn', defaultSeverity: 'info', description: '检测分支中多余的 progn', category: '风格' },
@@ -228,15 +233,30 @@ exports.RULES = [
228
233
  description: '检测 vlax-* 使用前未调用 vl-load-com',
229
234
  category: '最佳实践',
230
235
  },
231
- { name: 'unused_catch_result', defaultSeverity: 'info', description: '检测 vl-catch-all-apply 结果被当布尔值用', category: '正确性' },
236
+ {
237
+ name: 'unused_catch_result',
238
+ defaultSeverity: 'info',
239
+ description: '检测 vl-catch-all-apply 结果被当布尔值用',
240
+ category: '正确性',
241
+ },
232
242
  { name: 'string_concat_loop', defaultSeverity: 'info', description: '检测循环内字符串拼接', category: '性能' },
233
243
  { name: 'if_without_else', defaultSeverity: 'info', description: '检测 if 无 else 分支', category: '风格' },
234
244
  { name: 'redundant_list', defaultSeverity: 'info', description: '检测 (list) 等价于 nil', category: '风格' },
235
245
  { name: 'entget_in_loop', defaultSeverity: 'warn', description: '检测循环内 entget', category: '性能' },
236
- { name: 'promise_handler', defaultSeverity: 'info', description: '检测 vlax-invoke 缺少回调处理器', category: '最佳实践' },
246
+ {
247
+ name: 'promise_handler',
248
+ defaultSeverity: 'info',
249
+ description: '检测 vlax-invoke 缺少回调处理器',
250
+ category: '最佳实践',
251
+ },
237
252
  { name: 'module_cycle', defaultSeverity: 'warn', description: '检测模块循环依赖', category: '架构' },
238
253
  { name: 'arg_count_project', defaultSeverity: 'warn', description: '跨文件检查函数参数数量', category: '正确性' },
239
- { name: 'builtin_arg_count', defaultSeverity: 'error', description: '检查 AutoLISP 内置函数调用参数个数是否匹配', category: '正确性' },
254
+ {
255
+ name: 'builtin_arg_count',
256
+ defaultSeverity: 'error',
257
+ description: '检查 AutoLISP 内置函数调用参数个数是否匹配',
258
+ category: '正确性',
259
+ },
240
260
  ];
241
261
  function generateRulesMarkdown() {
242
262
  const lines = [
package/dist/runner.js CHANGED
@@ -166,15 +166,33 @@ function runChecks(content, file, config) {
166
166
  addIfEnabled('string_concat_loop', () => (0, string_concat_loop_1.checkStringConcatInLoop)(content, file));
167
167
  // AST-based checks via single-pass visitor
168
168
  const astRules = new Set([
169
- 'quit_exit', 'open_without_close', 'function_complexity',
170
- 'parameter_naming', 'unused_variable', 'unused_parameter',
171
- 'constant_condition', 'redundant_progn', 'empty_branch',
172
- 'unused_let_binding', 'recursive_call', 'variable_shadow',
173
- 'unused_local_fun', 'multiple_setq', 'redundant_quotes',
174
- 'redundant_setq', 'redundant_nil_else', 'single_arg_and_or',
175
- 'redundant_let', 'self_compare', 'misplaced_else',
176
- 'quote_vs_function', 'double_not', 'setq_single_arg',
177
- 'assoc_without_cdr', 'identical_branches', 'while_constant',
169
+ 'quit_exit',
170
+ 'open_without_close',
171
+ 'function_complexity',
172
+ 'parameter_naming',
173
+ 'unused_variable',
174
+ 'unused_parameter',
175
+ 'constant_condition',
176
+ 'redundant_progn',
177
+ 'empty_branch',
178
+ 'unused_let_binding',
179
+ 'recursive_call',
180
+ 'variable_shadow',
181
+ 'unused_local_fun',
182
+ 'multiple_setq',
183
+ 'redundant_quotes',
184
+ 'redundant_setq',
185
+ 'redundant_nil_else',
186
+ 'single_arg_and_or',
187
+ 'redundant_let',
188
+ 'self_compare',
189
+ 'misplaced_else',
190
+ 'quote_vs_function',
191
+ 'double_not',
192
+ 'setq_single_arg',
193
+ 'assoc_without_cdr',
194
+ 'identical_branches',
195
+ 'while_constant',
178
196
  'cond_duplicate',
179
197
  ]);
180
198
  const needsAst = Array.from(astRules).some(r => checks[r] !== 'off');
@@ -317,13 +335,28 @@ function extractForm(s, start) {
317
335
  }
318
336
  // ===== FIX APPLICATION =====
319
337
  exports.FIXABLE_RULES = new Set([
320
- 'redundant_quotes', 'double_not', 'redundant_nil_else',
321
- 'single_arg_and_or', 'redundant_setq', 'redundant_progn',
322
- 'redundant_let', 'quote_style', 'redundant_if', 'misplaced_else',
323
- 'setq_multiple', 'append_single', 'nth_usage', 'setq_single_arg',
324
- 'extra_parens', 'empty_branch',
325
- 'comment_style', 'eq_usage', 'cond_simplify', 'self_compare',
326
- 'while_constant', 'redundant_list',
338
+ 'redundant_quotes',
339
+ 'double_not',
340
+ 'redundant_nil_else',
341
+ 'single_arg_and_or',
342
+ 'redundant_setq',
343
+ 'redundant_progn',
344
+ 'redundant_let',
345
+ 'quote_style',
346
+ 'redundant_if',
347
+ 'misplaced_else',
348
+ 'setq_multiple',
349
+ 'append_single',
350
+ 'nth_usage',
351
+ 'setq_single_arg',
352
+ 'extra_parens',
353
+ 'empty_branch',
354
+ 'comment_style',
355
+ 'eq_usage',
356
+ 'cond_simplify',
357
+ 'self_compare',
358
+ 'while_constant',
359
+ 'redundant_list',
327
360
  ]);
328
361
  function applyFixes(issues, content, filepath) {
329
362
  const lines = content.split('\n');
package/dist/validate.js CHANGED
@@ -85,6 +85,7 @@ const VALID_RULES = [
85
85
  'promise_handler',
86
86
  'module_cycle',
87
87
  'arg_count_project',
88
+ 'builtin_arg_count',
88
89
  ];
89
90
  const VALID_SEVERITIES = ['off', 'info', 'warn', 'error'];
90
91
  function validateConfig(config) {
@@ -4,11 +4,6 @@ exports.runChecksWithVisitor = runChecksWithVisitor;
4
4
  const locale_1 = require("./locale");
5
5
  const disable_1 = require("./disable");
6
6
  const parser_1 = require("@atlisp/parser");
7
- function emit(issues, rule, line, severity, message, file) {
8
- if (severity === 'off')
9
- return;
10
- issues.push({ file, line, severity: severity, rule, message });
11
- }
12
7
  function runChecksWithVisitor(ast, file, config, disableMap) {
13
8
  const issues = [];
14
9
  const checks = config.checks;
@@ -34,7 +29,11 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
34
29
  function isInQuote(node) {
35
30
  let p = node.parent;
36
31
  while (p) {
37
- if (p.type === 'list' && p.children && p.children.length > 0 && p.children[0].type === 'symbol' && (p.children[0].name === 'quote' || p.children[0].name === 'function')) {
32
+ if (p.type === 'list' &&
33
+ p.children &&
34
+ p.children.length > 0 &&
35
+ p.children[0].type === 'symbol' &&
36
+ (p.children[0].name === 'quote' || p.children[0].name === 'function')) {
38
37
  return true;
39
38
  }
40
39
  p = p.parent;
@@ -82,7 +81,11 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
82
81
  }
83
82
  function hasSelfCall(body, fnName) {
84
83
  for (const form of body) {
85
- if (form.type === 'list' && form.children && form.children.length > 0 && form.children[0].type === 'symbol' && form.children[0].name === fnName) {
84
+ if (form.type === 'list' &&
85
+ form.children &&
86
+ form.children.length > 0 &&
87
+ form.children[0].type === 'symbol' &&
88
+ form.children[0].name === fnName) {
86
89
  return true;
87
90
  }
88
91
  }
@@ -143,8 +146,16 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
143
146
  }
144
147
  }
145
148
  const higherOrderFuncs = new Set([
146
- 'mapcar', 'apply', 'lambda', 'vl-sort', 'vl-sort-i',
147
- 'vl-remove-if', 'vl-remove-if-not', 'vl-member-if', 'vl-some', 'vl-every',
149
+ 'mapcar',
150
+ 'apply',
151
+ 'lambda',
152
+ 'vl-sort',
153
+ 'vl-sort-i',
154
+ 'vl-remove-if',
155
+ 'vl-remove-if-not',
156
+ 'vl-member-if',
157
+ 'vl-some',
158
+ 'vl-every',
148
159
  ]);
149
160
  // redundant_progn
150
161
  if (checks['redundant_progn'] !== 'off') {
@@ -220,9 +231,13 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
220
231
  // double_not
221
232
  if (checks['double_not'] !== 'off') {
222
233
  visitor.on('not', node => {
223
- if (node.children && node.children.length >= 2 && node.children[1].type === 'list' &&
224
- node.children[1].children && node.children[1].children.length > 0 &&
225
- node.children[1].children[0].type === 'symbol' && node.children[1].children[0].name === 'not') {
234
+ if (node.children &&
235
+ node.children.length >= 2 &&
236
+ node.children[1].type === 'list' &&
237
+ node.children[1].children &&
238
+ node.children[1].children.length > 0 &&
239
+ node.children[1].children[0].type === 'symbol' &&
240
+ node.children[1].children[0].name === 'not') {
226
241
  addIssue('double_not', node.pos.line, (0, locale_1.t)('double_not'));
227
242
  }
228
243
  });
@@ -249,9 +264,12 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
249
264
  visitor.on('if', node => {
250
265
  if (node.children && node.children.length === 4) {
251
266
  const cond = node.children[1];
252
- if (cond.type === 'list' && cond.children && cond.children.length >= 2 &&
253
- cond.children[0].type === 'symbol' && cond.children[0].name === 'not') {
254
- const innerName = cond.children[1].type === 'symbol' ? (cond.children[1].name || '') : '';
267
+ if (cond.type === 'list' &&
268
+ cond.children &&
269
+ cond.children.length >= 2 &&
270
+ cond.children[0].type === 'symbol' &&
271
+ cond.children[0].name === 'not') {
272
+ const innerName = cond.children[1].type === 'symbol' ? cond.children[1].name || '' : '';
255
273
  addIssue('misplaced_else', node.pos.line, (0, locale_1.t)('misplaced_else', innerName));
256
274
  }
257
275
  }
@@ -263,7 +281,7 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
263
281
  if (node.children && node.children.length >= 2) {
264
282
  const cond = node.children[1];
265
283
  if (isLiteral(cond)) {
266
- const name = cond.type === 'symbol' ? (cond.name || '') : String(cond.value ?? '');
284
+ const name = cond.type === 'symbol' ? cond.name || '' : String(cond.value ?? '');
267
285
  addIssue('while_constant', node.pos.line, (0, locale_1.t)('while_constant', name));
268
286
  }
269
287
  }
@@ -275,7 +293,7 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
275
293
  if (node.children && node.children.length >= 2) {
276
294
  const cond = node.children[1];
277
295
  if (isLiteral(cond)) {
278
- const name = cond.type === 'symbol' ? (cond.name || '') : String(cond.value ?? '');
296
+ const name = cond.type === 'symbol' ? cond.name || '' : String(cond.value ?? '');
279
297
  addIssue('constant_condition', node.pos.line, (0, locale_1.t)('constant_condition', name, formName));
280
298
  }
281
299
  }
@@ -288,7 +306,7 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
288
306
  if (clause.type === 'list' && clause.children && clause.children.length >= 2) {
289
307
  const test = clause.children[1];
290
308
  if (isLiteral(test)) {
291
- const name = test.type === 'symbol' ? (test.name || '') : String(test.value ?? '');
309
+ const name = test.type === 'symbol' ? test.name || '' : String(test.value ?? '');
292
310
  addIssue('constant_condition', node.pos.line, (0, locale_1.t)('constant_condition', name, 'cond'));
293
311
  }
294
312
  }
@@ -298,9 +316,13 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
298
316
  // quote_vs_function
299
317
  if (checks['quote_vs_function'] !== 'off') {
300
318
  visitor.on('quote', node => {
301
- if (node.children && node.children.length >= 2 && node.children[1].type === 'list' &&
302
- node.children[1].children && node.children[1].children.length > 0 &&
303
- node.children[1].children[0].type === 'symbol' && node.children[1].children[0].name === 'lambda') {
319
+ if (node.children &&
320
+ node.children.length >= 2 &&
321
+ node.children[1].type === 'list' &&
322
+ node.children[1].children &&
323
+ node.children[1].children.length > 0 &&
324
+ node.children[1].children[0].type === 'symbol' &&
325
+ node.children[1].children[0].name === 'lambda') {
304
326
  const parent = node.parent;
305
327
  if (parent && parent.children && parent.children.length > 0) {
306
328
  const pName = (parent.children[0].type === 'symbol' && parent.children[0].name) || '';
@@ -314,9 +336,13 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
314
336
  // redundant_quotes
315
337
  if (checks['redundant_quotes'] !== 'off') {
316
338
  visitor.on('quote', node => {
317
- if (node.children && node.children.length >= 2 && node.children[1].type === 'list' &&
318
- node.children[1].children && node.children[1].children.length > 0 &&
319
- node.children[1].children[0].type === 'symbol' && node.children[1].children[0].name === 'quote') {
339
+ if (node.children &&
340
+ node.children.length >= 2 &&
341
+ node.children[1].type === 'list' &&
342
+ node.children[1].children &&
343
+ node.children[1].children.length > 0 &&
344
+ node.children[1].children[0].type === 'symbol' &&
345
+ node.children[1].children[0].name === 'quote') {
320
346
  addIssue('redundant_quotes', node.pos.line, (0, locale_1.t)('redundant_quotes'));
321
347
  }
322
348
  });
@@ -347,8 +373,11 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
347
373
  addIssue('assoc_without_cdr', node.pos.line, (0, locale_1.t)('assoc_without_cdr'));
348
374
  return;
349
375
  }
350
- if (parent.type === 'list' && parent.children && parent.children.length > 0 &&
351
- parent.children[0].type === 'symbol' && (parent.children[0].name === 'cdr' || parent.children[0].name === 'cadr')) {
376
+ if (parent.type === 'list' &&
377
+ parent.children &&
378
+ parent.children.length > 0 &&
379
+ parent.children[0].type === 'symbol' &&
380
+ (parent.children[0].name === 'cdr' || parent.children[0].name === 'cadr')) {
352
381
  return;
353
382
  }
354
383
  addIssue('assoc_without_cdr', node.pos.line, (0, locale_1.t)('assoc_without_cdr'));
@@ -368,8 +397,12 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
368
397
  }
369
398
  // open_without_close (data collection)
370
399
  if (checks['open_without_close'] !== 'off') {
371
- visitor.on('open', () => { state.openCount++; });
372
- visitor.on('close', () => { state.closeCount++; });
400
+ visitor.on('open', () => {
401
+ state.openCount++;
402
+ });
403
+ visitor.on('close', () => {
404
+ state.closeCount++;
405
+ });
373
406
  }
374
407
  // cond_duplicate (data collection)
375
408
  if (checks['cond_duplicate'] !== 'off') {
@@ -404,8 +437,12 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
404
437
  dangerNames.add('vl-registry-write');
405
438
  if (dangerNames.size > 0) {
406
439
  visitor.on('*', node => {
407
- if (node.type === 'list' && node.children && node.children.length > 0 &&
408
- node.children[0].type === 'symbol' && node.children[0].name && dangerNames.has(node.children[0].name)) {
440
+ if (node.type === 'list' &&
441
+ node.children &&
442
+ node.children.length > 0 &&
443
+ node.children[0].type === 'symbol' &&
444
+ node.children[0].name &&
445
+ dangerNames.has(node.children[0].name)) {
409
446
  if (!isInQuote(node)) {
410
447
  state.dangerousCalls.push({ name: node.children[0].name, node });
411
448
  }
@@ -419,8 +456,11 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
419
456
  if (node.type === 'list' && node.children) {
420
457
  let prevSetq = false;
421
458
  for (const child of node.children) {
422
- if (child.type === 'list' && child.children && child.children.length > 0 &&
423
- child.children[0].type === 'symbol' && child.children[0].name === 'setq') {
459
+ if (child.type === 'list' &&
460
+ child.children &&
461
+ child.children.length > 0 &&
462
+ child.children[0].type === 'symbol' &&
463
+ child.children[0].name === 'setq') {
424
464
  if (prevSetq) {
425
465
  addIssue('multiple_setq', child.pos.line, (0, locale_1.t)('multiple_setq'));
426
466
  prevSetq = false;
@@ -460,15 +500,18 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
460
500
  return;
461
501
  const letVars = new Set();
462
502
  for (const b of bindings.children) {
463
- if (b.type === 'list' && b.children && b.children.length > 0 && b.children[0].type === 'symbol' && b.children[0].name) {
503
+ if (b.type === 'list' &&
504
+ b.children &&
505
+ b.children.length > 0 &&
506
+ b.children[0].type === 'symbol' &&
507
+ b.children[0].name) {
464
508
  letVars.add(b.children[0].name);
465
509
  }
466
510
  }
467
511
  // Walk body for setq of let-bound vars (shallow, don't descend into nested lets)
468
512
  const walkBodyForSetq = (forms) => {
469
513
  for (const form of forms) {
470
- if (form.type === 'list' && form.children && form.children.length > 0 &&
471
- form.children[0].type === 'symbol') {
514
+ if (form.type === 'list' && form.children && form.children.length > 0 && form.children[0].type === 'symbol') {
472
515
  if (form.children[0].name === 'setq' && form.children.length > 1) {
473
516
  for (let i = 1; i < form.children.length; i += 2) {
474
517
  const child = form.children[i];
@@ -501,7 +544,11 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
501
544
  return;
502
545
  const letVars = [];
503
546
  for (const b of bindings.children) {
504
- if (b.type === 'list' && b.children && b.children.length > 0 && b.children[0].type === 'symbol' && b.children[0].name) {
547
+ if (b.type === 'list' &&
548
+ b.children &&
549
+ b.children.length > 0 &&
550
+ b.children[0].type === 'symbol' &&
551
+ b.children[0].name) {
505
552
  letVars.push({ name: b.children[0].name, line: b.children[0].pos.line });
506
553
  }
507
554
  }
@@ -524,7 +571,7 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
524
571
  if (!node.children || node.children.length < 4)
525
572
  return;
526
573
  const fnName = node.children[1].type === 'symbol' ? node.children[1].name : '';
527
- if (!fnName || (fnName.startsWith('C:') || fnName.startsWith('c:')))
574
+ if (!fnName || fnName.startsWith('C:') || fnName.startsWith('c:'))
528
575
  return;
529
576
  const body = node.children.slice(3);
530
577
  if (checks['unused_parameter'] !== 'off') {
@@ -564,7 +611,13 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
564
611
  return;
565
612
  for (let i = 1; i < node.children.length; i += 2) {
566
613
  const varNode = node.children[i];
567
- if (varNode.type === 'symbol' && varNode.name && !varNode.name.startsWith('*') && !varNode.name.includes(':*') && varNode.name !== 'T' && varNode.name !== 'nil' && !varNode.name.startsWith('/')) {
614
+ if (varNode.type === 'symbol' &&
615
+ varNode.name &&
616
+ !varNode.name.startsWith('*') &&
617
+ !varNode.name.includes(':*') &&
618
+ varNode.name !== 'T' &&
619
+ varNode.name !== 'nil' &&
620
+ !varNode.name.startsWith('/')) {
568
621
  state.setqDefs.set(varNode.name, { line: varNode.pos.line, file });
569
622
  }
570
623
  }
@@ -598,7 +651,10 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
598
651
  visitor.on('if', node => {
599
652
  if (node.children && node.children.length === 3) {
600
653
  const parent = node.parent;
601
- if (parent && parent.type === 'list' && parent.children && parent.children.length > 0 &&
654
+ if (parent &&
655
+ parent.type === 'list' &&
656
+ parent.children &&
657
+ parent.children.length > 0 &&
602
658
  parent.children[0].type === 'symbol') {
603
659
  const parentName = parent.children[0].name;
604
660
  if (parentName === 'cond' || parentName === 'if')
@@ -619,19 +675,28 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
619
675
  // promise_handler
620
676
  if (checks['promise_handler'] !== 'off') {
621
677
  visitor.on('*', node => {
622
- if (node.type === 'list' && node.children && node.children.length > 0 &&
623
- node.children[0].type === 'symbol' && node.children[0].name &&
678
+ if (node.type === 'list' &&
679
+ node.children &&
680
+ node.children.length > 0 &&
681
+ node.children[0].type === 'symbol' &&
682
+ node.children[0].name &&
624
683
  (node.children[0].name.startsWith('vlax-invoke') || node.children[0].name.startsWith('vlax-invoke-method'))) {
625
684
  if (isInQuote(node))
626
685
  return;
627
686
  const parent = node.parent;
628
- if (parent && parent.type === 'list' && parent.children && parent.children.length > 0 &&
629
- parent.children[0].type === 'symbol' && parent.children[0].name === 'vl-catch-all-apply') {
687
+ if (parent &&
688
+ parent.type === 'list' &&
689
+ parent.children &&
690
+ parent.children.length > 0 &&
691
+ parent.children[0].type === 'symbol' &&
692
+ parent.children[0].name === 'vl-catch-all-apply') {
630
693
  return;
631
694
  }
632
695
  let hasCallback = false;
633
696
  for (const child of (node.children || []).slice(1)) {
634
- if (child.type === 'list' && child.children && child.children.length > 0 &&
697
+ if (child.type === 'list' &&
698
+ child.children &&
699
+ child.children.length > 0 &&
635
700
  child.children[0].type === 'symbol' &&
636
701
  (child.children[0].name === 'lambda' || child.children[0].name === 'function')) {
637
702
  hasCallback = true;
@@ -654,15 +719,19 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
654
719
  }
655
720
  // dangerous_calls
656
721
  if (checks['quit_exit'] !== 'off') {
657
- const dc = config.dangerous_calls;
658
722
  for (const call of state.dangerousCalls) {
659
723
  const sev = config.dangerous_calls[call.name === 'vl-registry-write' ? 'vl_registry_write' : call.name] || 'off';
660
724
  if (sev === 'off')
661
725
  continue;
662
- const msgKey = call.name === 'quit' ? 'dangerous.quit' :
663
- call.name === 'exit' ? 'dangerous.exit' :
664
- call.name === 'startapp' ? 'dangerous.startapp' :
665
- call.name === 'vl-registry-write' ? 'dangerous.vl_registry_write' : '';
726
+ const msgKey = call.name === 'quit'
727
+ ? 'dangerous.quit'
728
+ : call.name === 'exit'
729
+ ? 'dangerous.exit'
730
+ : call.name === 'startapp'
731
+ ? 'dangerous.startapp'
732
+ : call.name === 'vl-registry-write'
733
+ ? 'dangerous.vl_registry_write'
734
+ : '';
666
735
  if (msgKey) {
667
736
  addIssue(call.name === 'vl-registry-write' ? 'vl_registry_write' : call.name, call.node.pos.line, (0, locale_1.t)(msgKey));
668
737
  }
@@ -682,7 +751,6 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
682
751
  // function_complexity
683
752
  if (checks['function_complexity'] !== 'off') {
684
753
  const maxLines = config.function_complexity.max_lines;
685
- const maxNesting = config.function_complexity.max_nesting;
686
754
  for (const scope of state.defunScopes) {
687
755
  let maxLine = scope.line;
688
756
  const computeEnd = (nodes) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/lint",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "AutoLISP static analysis tool — parens, security, conventions, SBCL syntax validation",
5
5
  "keywords": [
6
6
  "CAD",
@@ -37,15 +37,15 @@
37
37
  "prepack": "npm run build"
38
38
  },
39
39
  "dependencies": {
40
- "@atlisp/parser": "^0.1.3"
40
+ "@atlisp/parser": "^0.1.5"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/jest": "^29",
44
- "@types/node": "^18",
45
- "@typescript-eslint/eslint-plugin": "^7",
46
- "@typescript-eslint/parser": "^7",
43
+ "@types/jest": "^30",
44
+ "@types/node": "^22",
45
+ "@typescript-eslint/eslint-plugin": "^8",
46
+ "@typescript-eslint/parser": "^8",
47
47
  "eslint": "^8",
48
- "jest": "^29",
48
+ "jest": "^30",
49
49
  "prettier": "^3",
50
50
  "ts-jest": "^29",
51
51
  "ts-node": "^10.9.2",
@@ -1,90 +0,0 @@
1
- {
2
- "locale": "zh",
3
- "source": {
4
- "globs": ["**/*.lsp"],
5
- "exclude": ["**/node_modules/**", "**/vendor/**", "**/.git/**"]
6
- },
7
- "checks": {
8
- "parens": "error",
9
- "encoding": "warn",
10
- "cl_syntax": "warn",
11
- "quit_exit": "error",
12
- "command_shell": "error",
13
- "startapp": "warn",
14
- "vl_registry_write": "warn",
15
- "vlax_without_loading": "off",
16
- "token_in_url": "warn",
17
- "open_without_close": "warn",
18
- "bare_function_names": "off",
19
- "line_length": "warn",
20
- "function_complexity": "warn",
21
- "parameter_naming": "warn",
22
- "unused_variable": "warn",
23
- "missing_doc": "warn",
24
- "error_handling": "warn",
25
- "global_naming": "warn",
26
- "extra_parens": "warn",
27
- "arg_count": "warn",
28
- "strcat_usage": "warn",
29
- "cond_simplify": "warn",
30
- "redundant_progn": "warn",
31
- "quote_style": "warn",
32
- "eq_usage": "warn",
33
- "lambda_syntax": "warn",
34
- "comment_style": "warn",
35
- "empty_catch": "warn",
36
- "nth_usage": "warn",
37
- "append_single": "warn",
38
- "setq_multiple": "warn",
39
- "function_order": "off",
40
- "magic_number": "off",
41
- "mixed_indent": "warn",
42
- "long_function_call": "warn",
43
- "no_return": "warn",
44
- "shadow_builtin": "warn",
45
- "dynamic_doc": "warn",
46
- "loop_optimization": "off",
47
- "type_check": "off",
48
- "redundant_if": "warn",
49
- "trailing_whitespace": "warn",
50
- "module_registration": "off",
51
- "namespace_header": "off"
52
- },
53
- "line_length": {
54
- "max": 100,
55
- "tab_width": 2
56
- },
57
- "function_complexity": {
58
- "max_lines": 60,
59
- "max_nesting": 6
60
- },
61
- "cl_syntax": {
62
- "keywords": ["&key"]
63
- },
64
- "dangerous_calls": {
65
- "quit": "error",
66
- "exit": "error",
67
- "command_shell": "error",
68
- "startapp": "warn",
69
- "vl_registry_write": "warn"
70
- },
71
- "module_registration": {
72
- "severity": "off",
73
- "patterns": [],
74
- "dirs": ["modules"]
75
- },
76
- "namespace_header": {
77
- "severity": "off",
78
- "package": "",
79
- "deps": [],
80
- "search_lines": 15
81
- },
82
- "bare_function_names": {
83
- "allowlist": ["T", "nil"],
84
- "namespace_pattern": "^[a-z]+:"
85
- },
86
- "sbcl": {
87
- "walk_exclude": [".vscode", "vendor", ".git"],
88
- "defmacro_allow_files": ["compat-cl"]
89
- }
90
- }
@@ -1,161 +0,0 @@
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)
@@ -1,41 +0,0 @@
1
- ((AUTOLISP ())
2
- (@ (COMMON-LISP AUTOLISP))
3
- (C ())
4
- (JSON ())
5
- (SIDEBAR ())
6
- (FUN ())
7
- (DCL ())
8
- (BASE ())
9
- (STRING ())
10
- (BLOCK ())
11
- (ENTITY ())
12
- (LAYER ())
13
- (LAYOUT ())
14
- (CURVE ())
15
- (LIST ())
16
- (M ())
17
- (P ())
18
- (UI ())
19
- (SYS ())
20
- (EXCEL ())
21
- (WORD ())
22
- (RE ())
23
- (VECTRA ())
24
- (DOS ())
25
- (AT-PM ())
26
- (QRENCODE ())
27
- (NETWORK ())
28
- (PKGMAN ())
29
- (DATETIME ())
30
- (DICT ())
31
- (@NLP ())
32
- (AT-SIDEBAR ())
33
- (S ())
34
- (VL ())
35
- (VLR ())
36
- (VLAX ())
37
- (THEME ())
38
- (AJAX ())
39
- (ATLISP ())
40
- (LOG ())
41
- (ATLISP-CORE ()))