@atlisp/lint 0.1.15 → 0.1.17

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.
@@ -63,7 +63,7 @@
63
63
  "cond_simplify": "warn",
64
64
  "quote_style": "warn",
65
65
  "eq_usage": "warn",
66
- "lambda_syntax": "warn",
66
+ "lambda_syntax": "off",
67
67
  "comment_style": "warn",
68
68
  "empty_catch": "warn",
69
69
  "nth_usage": "warn",
@@ -79,7 +79,8 @@
79
79
  "dynamic_doc": "warn",
80
80
  "loop_optimization": "off",
81
81
  "type_check": "off",
82
- "redundant_if": "warn"
82
+ "redundant_if": "warn",
83
+ "undeclared_setq": "warn"
83
84
  },
84
85
  "line_length": {
85
86
  "max": 100,
@@ -87,7 +88,7 @@
87
88
  },
88
89
  "function_complexity": {
89
90
  "max_lines": 60,
90
- "max_nesting": 6
91
+ "max_nesting": 15
91
92
  },
92
93
  "cl_syntax": {
93
94
  "keywords": ["&key"]
@@ -0,0 +1,90 @@
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
+ }
@@ -2,119 +2,50 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkArgCount = checkArgCount;
4
4
  const locale_1 = require("../locale");
5
- const utils_1 = require("../utils");
5
+ const parser_1 = require("@atlisp/parser");
6
6
  function checkArgCount(content, file) {
7
7
  const issues = [];
8
- const lines = content.split('\n');
9
- const defuns = [];
10
- // Collect all defun definitions
11
- for (let i = 0; i < lines.length; i++) {
12
- const stripped = (0, utils_1.stripLine)(lines[i]);
13
- const m = stripped.match(/\(defun\s+(\S+)\s+\(([^)]*)\)/);
14
- if (m) {
15
- const all = m[2];
16
- const slashIdx = all.indexOf('/');
17
- const paramStr = slashIdx >= 0 ? all.slice(0, slashIdx).trim() : all;
18
- const params = paramStr.split(/\s+/).filter(Boolean);
19
- defuns.push({ name: m[1], line: i + 1, paramCount: params.length });
20
- }
21
- }
22
- // Check calls against definitions
23
- for (const fn of defuns) {
24
- if (fn.name.startsWith('c:') || fn.name.includes('/'))
8
+ const ast = (0, parser_1.parseAst)(content);
9
+ const defuns = (0, parser_1.astFindListWithHead)(ast, 'defun');
10
+ for (const defun of defuns) {
11
+ if (!defun.children || defun.children.length < 3)
12
+ continue;
13
+ const nameNode = defun.children[1];
14
+ if (nameNode.type !== 'symbol' || !nameNode.name)
15
+ continue;
16
+ const funcName = nameNode.name;
17
+ // Skip C: commands and local functions
18
+ if (funcName.startsWith('c:') || funcName.startsWith('C:') || funcName.includes('/'))
25
19
  continue;
26
- if (fn.paramCount === 0)
20
+ const paramList = defun.children[2];
21
+ if (paramList.type !== 'list' || !paramList.children)
22
+ continue;
23
+ // Count params before /
24
+ let paramCount = 0;
25
+ for (const p of paramList.children) {
26
+ if (p.type === 'symbol' && p.name === '/')
27
+ break;
28
+ if (p.type === 'symbol' && p.name && p.name !== 'T' && p.name !== 'nil') {
29
+ paramCount++;
30
+ }
31
+ }
32
+ if (paramCount === 0)
27
33
  continue;
28
- for (let i = 0; i < lines.length; i++) {
29
- const stripped = (0, utils_1.stripLine)(lines[i]);
30
- const re = new RegExp('\\(\\s*' + fn.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s+');
31
- // Don't match the definition line
32
- if (stripped.includes('defun') && stripped.includes(fn.name))
34
+ // Find all calls to this function
35
+ const calls = (0, parser_1.astFindListWithHead)(ast, funcName);
36
+ for (const call of calls) {
37
+ if (!call.children || call.children.length === 0)
33
38
  continue;
34
- const callMatch = re.exec(stripped);
35
- if (callMatch) {
36
- // Collect all text from the call across lines
37
- let callText = stripped.slice(callMatch.index);
38
- let parenDepth = 0;
39
- for (const ch of callText) {
40
- if (ch === '(')
41
- parenDepth++;
42
- else if (ch === ')')
43
- parenDepth--;
44
- }
45
- // If not closed on this line, read next lines
46
- let lineIdx = i + 1;
47
- while (parenDepth > 0 && lineIdx < lines.length) {
48
- const nextStripped = (0, utils_1.stripLine)(lines[lineIdx]);
49
- callText += ' ' + nextStripped;
50
- for (const ch of nextStripped) {
51
- if (ch === '(')
52
- parenDepth++;
53
- else if (ch === ')')
54
- parenDepth--;
55
- }
56
- lineIdx++;
57
- }
58
- // Extract args: remove the function call prefix
59
- const afterName = callText.slice(callMatch[0].length - 1); // keep the leading (
60
- // Find matching closing paren
61
- let closeIdx = -1;
62
- let pd = 1;
63
- for (let j = 0; j < afterName.length; j++) {
64
- if (afterName[j] === '(')
65
- pd++;
66
- else if (afterName[j] === ')') {
67
- pd--;
68
- if (pd === 0) {
69
- closeIdx = j;
70
- break;
71
- }
72
- }
73
- }
74
- const argsStr = closeIdx >= 0 ? afterName.slice(0, closeIdx).trim() : afterName.trim();
75
- if (!argsStr)
76
- continue;
77
- // Count space-delimited args at depth 0
78
- let argCount = 0;
79
- let depth = 0;
80
- let inArg = false;
81
- for (const ch of argsStr) {
82
- if (ch === '(') {
83
- depth++;
84
- if (!inArg) {
85
- inArg = true;
86
- }
87
- }
88
- else if (ch === ')') {
89
- depth--;
90
- if (depth === 0) {
91
- if (inArg) {
92
- argCount++;
93
- inArg = false;
94
- }
95
- }
96
- }
97
- else if (depth === 0 && (ch === ' ' || ch === '\t')) {
98
- if (inArg) {
99
- argCount++;
100
- inArg = false;
101
- }
102
- }
103
- else if (!inArg) {
104
- inArg = true;
105
- }
106
- }
107
- if (inArg)
108
- argCount++;
109
- if (argCount !== fn.paramCount) {
110
- issues.push({
111
- file,
112
- line: i + 1,
113
- severity: 'warn',
114
- rule: 'arg_count',
115
- message: (0, locale_1.t)('arg_count', fn.name, argCount, fn.paramCount),
116
- });
117
- }
39
+ // Skip if the first child is the function name — that's the head of this call
40
+ const argCount = call.children.length - 1;
41
+ if (argCount !== paramCount) {
42
+ issues.push({
43
+ file,
44
+ line: call.pos.line,
45
+ severity: 'warn',
46
+ rule: 'arg_count',
47
+ message: (0, locale_1.t)('arg_count', funcName, argCount, paramCount),
48
+ });
118
49
  }
119
50
  }
120
51
  }
@@ -16,6 +16,8 @@ function checkBareFunctionNames(content, file, allowlist, namespacePattern) {
16
16
  continue;
17
17
  if (name.includes('/'))
18
18
  continue; // local: defun foo/bar
19
+ if (name.startsWith('cb-'))
20
+ continue; // DCL callback
19
21
  if (!nsRegex.test(name) && !name.includes(':')) {
20
22
  issues.push({
21
23
  file,
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkGlobalNaming = checkGlobalNaming;
4
4
  const locale_1 = require("../locale");
5
5
  const parser_1 = require("@atlisp/parser");
6
+ /** Check if a variable name is earmuffed (*var*), optionally with a namespace prefix (ns:*var*) */
7
+ function isEarmuffed(name) {
8
+ const localName = name.includes(':') ? name.slice(name.lastIndexOf(':') + 1) : name;
9
+ return localName.startsWith('*') && localName.endsWith('*') && localName.length > 2;
10
+ }
6
11
  function isInsideLocalScope(node) {
7
12
  let cur = node.parent;
8
13
  while (cur) {
@@ -34,7 +39,7 @@ function checkGlobalNaming(content, file) {
34
39
  continue;
35
40
  if (v.name === 'T' || v.name === 'nil')
36
41
  continue;
37
- if (v.name.startsWith('*') && v.name.endsWith('*'))
42
+ if (isEarmuffed(v.name))
38
43
  continue;
39
44
  issues.push({
40
45
  file,
@@ -44,6 +44,7 @@ const loop_optimization_1 = require("./loop-optimization");
44
44
  const type_check_1 = require("./type-check");
45
45
  const redundant_if_1 = require("./redundant-if");
46
46
  const trailing_ws_1 = require("./trailing-ws");
47
+ const undeclared_setq_1 = require("./undeclared-setq");
47
48
  function runChecks(content, file, config) {
48
49
  const issues = [];
49
50
  const disableMap = (0, disable_1.parseDisableComments)(content);
@@ -108,6 +109,7 @@ function runChecks(content, file, config) {
108
109
  { rule: 'type_check', fn: () => (0, type_check_1.checkTypeCheck)(content, file) },
109
110
  { rule: 'redundant_if', fn: () => (0, redundant_if_1.checkRedundantIf)(content, file) },
110
111
  { rule: 'trailing_whitespace', fn: () => (0, trailing_ws_1.checkTrailingWhitespace)(content, file) },
112
+ { rule: 'undeclared_setq', fn: () => (0, undeclared_setq_1.checkUndeclaredSetq)(content, file) },
111
113
  ];
112
114
  for (const c of checks) {
113
115
  addIfEnabled(c.rule, c.fn);
@@ -2,48 +2,31 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkNoReturn = checkNoReturn;
4
4
  const locale_1 = require("../locale");
5
- const utils_1 = require("../utils");
5
+ const parser_1 = require("@atlisp/parser");
6
6
  function checkNoReturn(content, file) {
7
7
  const issues = [];
8
- const lines = content.split('\n');
9
- let inDefun = false;
10
- let defunName = '';
11
- let defunStart = 0;
12
- let depth = 0;
13
- let hasReturn = false;
14
- for (let i = 0; i < lines.length; i++) {
15
- const stripped = (0, utils_1.stripLine)(lines[i]);
16
- if (!inDefun) {
17
- const m = stripped.match(/\(defun\s+(\S+)/);
18
- if (m) {
19
- inDefun = true;
20
- defunName = m[1];
21
- defunStart = i + 1;
22
- depth = 0;
23
- hasReturn = false;
24
- }
25
- }
26
- if (inDefun) {
27
- for (const ch of stripped) {
28
- if (ch === '(')
29
- depth++;
30
- else if (ch === ')')
31
- depth--;
32
- }
33
- if (stripped.trim().startsWith('(') && !stripped.includes('defun'))
34
- hasReturn = true;
35
- if (depth === 0 && defunName && !defunName.startsWith('c:')) {
36
- if (!hasReturn) {
37
- issues.push({
38
- file,
39
- line: defunStart,
40
- severity: 'warn',
41
- rule: 'no_return',
42
- message: (0, locale_1.t)('no_return', defunName),
43
- });
44
- }
45
- inDefun = false;
46
- }
8
+ const ast = (0, parser_1.parseAst)(content);
9
+ const defuns = (0, parser_1.astFindListWithHead)(ast, 'defun');
10
+ for (const defun of defuns) {
11
+ if (!defun.children || defun.children.length < 3)
12
+ continue;
13
+ const nameNode = defun.children[1];
14
+ if (nameNode.type !== 'symbol' || !nameNode.name)
15
+ continue;
16
+ const funcName = nameNode.name;
17
+ // Skip C: commands — they don't need return values
18
+ if (/^[cC]:/.test(funcName))
19
+ continue;
20
+ // Body starts at index 3 (defun, name, params, body...)
21
+ const bodyExprs = defun.children.slice(3);
22
+ if (bodyExprs.length === 0) {
23
+ issues.push({
24
+ file,
25
+ line: defun.pos.line,
26
+ severity: 'warn',
27
+ rule: 'no_return',
28
+ message: (0, locale_1.t)('no_return', funcName),
29
+ });
47
30
  }
48
31
  }
49
32
  return issues;
@@ -8,6 +8,8 @@ function checkRecursiveCall(content, file) {
8
8
  const ast = (0, parser_1.parseAst)(content);
9
9
  return checkRecursiveCallAst(ast, file);
10
10
  }
11
+ /** Forms that provide conditional branching (exit condition for recursion) */
12
+ const CONDITIONAL_FORMS = new Set(['if', 'cond', 'when', 'unless']);
11
13
  function checkRecursiveCallAst(ast, file) {
12
14
  const issues = [];
13
15
  const defuns = (0, parser_1.astFindAll)(ast, n => (0, parser_1.astIsList)(n, 'defun'));
@@ -18,25 +20,30 @@ function checkRecursiveCallAst(ast, file) {
18
20
  const funcName = nameNode.type === 'symbol' && nameNode.name ? nameNode.name : '';
19
21
  if (!funcName)
20
22
  continue;
21
- // Walk the function body (skip name and param list)
23
+ // Check body for self-call (skip name and param list)
24
+ let hasRecursiveCall = false;
25
+ let hasConditional = false;
22
26
  for (let i = 3; i < defun.children.length; i++) {
23
- if (hasSelfCall(defun.children[i], funcName)) {
24
- issues.push({
25
- file,
26
- line: defun.pos.line,
27
- severity: 'warn',
28
- rule: 'recursive_call',
29
- message: (0, locale_1.t)('recursive_call', funcName),
30
- });
31
- break; // one issue per function
32
- }
27
+ if (hasSelfCall(defun.children[i], funcName))
28
+ hasRecursiveCall = true;
29
+ if (hasConditionalForm(defun.children[i]))
30
+ hasConditional = true;
31
+ }
32
+ // Only warn if the function calls itself AND has no exit condition
33
+ if (hasRecursiveCall && !hasConditional) {
34
+ issues.push({
35
+ file,
36
+ line: defun.pos.line,
37
+ severity: 'warn',
38
+ rule: 'recursive_call',
39
+ message: (0, locale_1.t)('recursive_call', funcName),
40
+ });
33
41
  }
34
42
  }
35
43
  return issues;
36
44
  }
37
45
  function hasSelfCall(node, funcName) {
38
46
  for (const n of (0, parser_1.astWalk)(node)) {
39
- // Check if this node is a list whose car is the function name
40
47
  if (n.type === 'list' && n.children && n.children.length > 0) {
41
48
  const car = n.children[0];
42
49
  if (car.type === 'symbol' && car.name === funcName) {
@@ -46,4 +53,15 @@ function hasSelfCall(node, funcName) {
46
53
  }
47
54
  return false;
48
55
  }
56
+ function hasConditionalForm(node) {
57
+ for (const n of (0, parser_1.astWalk)(node)) {
58
+ if (n.type === 'list' && n.children && n.children.length > 0) {
59
+ const car = n.children[0];
60
+ if (car.type === 'symbol' && car.name && CONDITIONAL_FORMS.has(car.name)) {
61
+ return true;
62
+ }
63
+ }
64
+ }
65
+ return false;
66
+ }
49
67
  //# sourceMappingURL=recursive-call.js.map
@@ -0,0 +1,3 @@
1
+ import { Issue } from '../types';
2
+ export declare function checkUndeclaredSetq(content: string, file: string): Issue[];
3
+ //# sourceMappingURL=undeclared-setq.d.ts.map
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkUndeclaredSetq = checkUndeclaredSetq;
4
+ const locale_1 = require("../locale");
5
+ const parser_1 = require("@atlisp/parser");
6
+ /** Extract declared variable names from a defun's parameter list (both sides of /) */
7
+ function getDeclaredVars(defun) {
8
+ const vars = new Set();
9
+ if (!defun.children || defun.children.length < 3)
10
+ return vars;
11
+ const paramList = defun.children[2];
12
+ if (paramList.type !== 'list' || !paramList.children)
13
+ return vars;
14
+ for (const p of paramList.children) {
15
+ if (p.type === 'symbol' && p.name && p.name !== '/' && p.name !== 'T' && p.name !== 'nil') {
16
+ vars.add(p.name);
17
+ }
18
+ }
19
+ return vars;
20
+ }
21
+ /** Check if a variable name is earmuffed (*var*), optionally with a namespace prefix (ns:*var*) */
22
+ function isEarmuffed(name) {
23
+ const localName = name.includes(':') ? name.slice(name.lastIndexOf(':') + 1) : name;
24
+ return localName.startsWith('*') && localName.endsWith('*') && localName.length > 2;
25
+ }
26
+ /** Find the nearest enclosing defun that is not inside a nested lambda/defun */
27
+ function findEnclosingDefun(node) {
28
+ let cur = node.parent;
29
+ while (cur) {
30
+ if (cur.type === 'list' && cur.children && cur.children.length > 0) {
31
+ const head = cur.children[0];
32
+ if (head.type === 'symbol') {
33
+ if (head.name === 'defun')
34
+ return cur;
35
+ if (head.name === 'lambda')
36
+ return null; // stopped by lambda boundary
37
+ }
38
+ }
39
+ cur = cur.parent;
40
+ }
41
+ return null;
42
+ }
43
+ function checkUndeclaredSetq(content, file) {
44
+ const issues = [];
45
+ const ast = (0, parser_1.parseAst)(content);
46
+ const setqForms = (0, parser_1.astFindListWithHead)(ast, 'setq');
47
+ for (const setq of setqForms) {
48
+ if (!setq.children || setq.children.length < 2)
49
+ continue;
50
+ // Skip top-level setq (not inside any defun)
51
+ const defun = findEnclosingDefun(setq);
52
+ if (!defun)
53
+ continue;
54
+ const declared = getDeclaredVars(defun);
55
+ for (let i = 1; i < setq.children.length; i += 2) {
56
+ const v = setq.children[i];
57
+ if (v.type !== 'symbol' || !v.name)
58
+ continue;
59
+ if (v.name === 'T' || v.name === 'nil')
60
+ continue;
61
+ if (isEarmuffed(v.name))
62
+ continue;
63
+ if (declared.has(v.name))
64
+ continue;
65
+ issues.push({
66
+ file,
67
+ line: v.pos.line,
68
+ severity: 'warn',
69
+ rule: 'undeclared_setq',
70
+ message: (0, locale_1.t)('undeclared_setq', v.name),
71
+ });
72
+ }
73
+ }
74
+ return issues;
75
+ }
76
+ //# sourceMappingURL=undeclared-setq.js.map
package/dist/config.js CHANGED
@@ -64,6 +64,7 @@ const DEFAULT_CONFIG = {
64
64
  unused_variable: 'warn',
65
65
  missing_doc: 'warn',
66
66
  trailing_whitespace: 'warn',
67
+ undeclared_setq: 'warn',
67
68
  module_registration: 'off',
68
69
  namespace_header: 'off',
69
70
  unused_parameter: 'warn',
@@ -105,7 +106,7 @@ const DEFAULT_CONFIG = {
105
106
  cond_simplify: 'warn',
106
107
  quote_style: 'warn',
107
108
  eq_usage: 'warn',
108
- lambda_syntax: 'warn',
109
+ lambda_syntax: 'off',
109
110
  comment_style: 'warn',
110
111
  empty_catch: 'warn',
111
112
  nth_usage: 'warn',
@@ -129,7 +130,7 @@ const DEFAULT_CONFIG = {
129
130
  },
130
131
  function_complexity: {
131
132
  max_lines: 60,
132
- max_nesting: 6,
133
+ max_nesting: 15,
133
134
  },
134
135
  cl_syntax: {
135
136
  keywords: ['&key'],
@@ -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)
package/dist/locale.js CHANGED
@@ -18,11 +18,13 @@ const messages = {
18
18
  vlax_without_loading: 'vlax-* call without vl-load-com in file',
19
19
  token_in_url: 'Possible token exposure in string: token= in URL',
20
20
  open_without_close: 'File has {0} open() calls but {1} close() calls (possible resource leak)',
21
+ arg_count: "Function '{0}' called with {1} arguments but defined with {2}",
21
22
  bare_function_names: "Unnamespaced function '{0}' (missing @:: or C: prefix)",
22
23
  module_registration: 'Module file missing @::*modules* registration (expected in last lines)',
23
24
  'namespace_header.missing': 'Missing (in-package {0}) header',
24
25
  'namespace_header.wrong': 'in-package target is not {0}',
25
26
  trailing_whitespace: 'Trailing whitespace',
27
+ undeclared_setq: "Variable '{0}' is setq'd inside defun but not declared in the parameter list — add to / list",
26
28
  line_length: 'Line length {0} exceeds {1} characters',
27
29
  'function_complexity.lines': "Function '{0}' has {1} lines (max {2})",
28
30
  'function_complexity.nesting': "Function '{0}' nesting depth {1} exceeds {2}",
@@ -64,6 +66,7 @@ const messages = {
64
66
  duplicate_defun: "Function '{0}' is defined multiple times",
65
67
  dangling_defun: "Function '{0}' is defined but never called",
66
68
  missing_export: "Function '{0}' is not registered in @::*modules*",
69
+ no_return: "Function '{0}' has no return value (body is empty)",
67
70
  unused_package_dep: "Imported package '{0}' is never referenced",
68
71
  'runner.read_error': 'Cannot read file',
69
72
  'sbcl.script_not_found': 'lint-sbcl.lisp not found at {0}',
@@ -127,11 +130,13 @@ Options:
127
130
  vlax_without_loading: '文件中使用了 vlax-* 函数但未调用 vl-load-com',
128
131
  token_in_url: '字符串中可能泄露令牌:URL 中包含 token=',
129
132
  open_without_close: '文件中有 {0} 个 open() 调用,但只有 {1} 个 close() 调用(可能存在资源泄露)',
133
+ arg_count: "函数 '{0}' 调用时传入了 {1} 个参数,但定义时为 {2} 个",
130
134
  bare_function_names: "函数 '{0}' 缺少命名空间前缀(缺少 @:: 或 C: 前缀)",
131
135
  module_registration: '模块文件缺少 @::*modules* 注册(应在文件末尾附近)',
132
136
  'namespace_header.missing': '缺少 (in-package {0}) 头',
133
137
  'namespace_header.wrong': 'in-package 目标不是 {0}',
134
138
  trailing_whitespace: '行尾有多余空格',
139
+ undeclared_setq: "变量 '{0}' 在 defun 内被 setq 但未声明到参数表中——建议加入 / 私有变量表",
135
140
  line_length: '行长度 {0} 超过 {1} 个字符',
136
141
  'function_complexity.lines': "函数 '{0}' 有 {1} 行(最大 {2} 行)",
137
142
  'function_complexity.nesting': "函数 '{0}' 嵌套深度 {1} 超过 {2}",
@@ -173,6 +178,7 @@ Options:
173
178
  duplicate_defun: "函数 '{0}' 被多次定义",
174
179
  dangling_defun: "函数 '{0}' 定义了但从未被调用",
175
180
  missing_export: "函数 '{0}' 未注册到 @::*modules*",
181
+ no_return: "函数 '{0}' 没有表达式——函数体为空",
176
182
  unused_package_dep: "导入的包 '{0}' 从未被引用",
177
183
  'runner.read_error': '无法读取文件',
178
184
  'sbcl.script_not_found': '未找到 lint-sbcl.lisp:{0}',
package/dist/presets.js CHANGED
@@ -54,7 +54,7 @@ exports.PRESETS = {
54
54
  cond_simplify: 'warn',
55
55
  quote_style: 'warn',
56
56
  eq_usage: 'warn',
57
- lambda_syntax: 'warn',
57
+ lambda_syntax: 'off',
58
58
  comment_style: 'warn',
59
59
  empty_catch: 'warn',
60
60
  nth_usage: 'warn',
package/dist/rules.js CHANGED
@@ -92,7 +92,7 @@ exports.RULES = [
92
92
  { name: 'global_naming', defaultSeverity: 'warn', description: '检测全局变量是否使用 *...* 命名', category: '风格' },
93
93
  {
94
94
  name: 'lambda_syntax',
95
- defaultSeverity: 'warn',
95
+ defaultSeverity: 'off',
96
96
  description: '检测 (function (lambda ...)) 建议使用缩写',
97
97
  category: '风格',
98
98
  },
@@ -189,6 +189,12 @@ exports.RULES = [
189
189
  { name: 'trailing_paren', defaultSeverity: 'warn', description: '检测多余闭合括号', category: '语法' },
190
190
  { name: 'trailing_whitespace', defaultSeverity: 'warn', description: '检测行尾多余空格', category: '风格' },
191
191
  { name: 'type_check', defaultSeverity: 'off', description: '检测 getvar 结果未做类型检查', category: '最佳实践' },
192
+ {
193
+ name: 'undeclared_setq',
194
+ defaultSeverity: 'warn',
195
+ description: '检测 defun 内 setq 了未声明的变量(建议加入 / 私有变量表)',
196
+ category: '正确性',
197
+ },
192
198
  {
193
199
  name: 'unused_let_binding',
194
200
  defaultSeverity: 'warn',
package/dist/runner.js CHANGED
@@ -111,6 +111,7 @@ const dynamic_doc_1 = require("./checks/dynamic-doc");
111
111
  const loop_optimization_1 = require("./checks/loop-optimization");
112
112
  const type_check_1 = require("./checks/type-check");
113
113
  const redundant_if_1 = require("./checks/redundant-if");
114
+ const undeclared_setq_1 = require("./checks/undeclared-setq");
114
115
  const format_indent_1 = require("./checks/format-indent");
115
116
  const config_1 = require("./config");
116
117
  const locale_1 = require("./locale");
@@ -235,6 +236,7 @@ function runChecks(content, file, config) {
235
236
  addIfEnabled('loop_optimization', () => (0, loop_optimization_1.checkLoopOptimization)(content, file));
236
237
  addIfEnabled('type_check', () => (0, type_check_1.checkTypeCheck)(content, file));
237
238
  addIfEnabled('redundant_if', () => (0, redundant_if_1.checkRedundantIf)(content, file));
239
+ addIfEnabled('undeclared_setq', () => (0, undeclared_setq_1.checkUndeclaredSetq)(content, file));
238
240
  addIfEnabled('format_indent', () => (0, format_indent_1.checkFormatIndent)(content, file, config.line_length.tab_width));
239
241
  return issues;
240
242
  }
@@ -0,0 +1,41 @@
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 ()))
package/dist/validate.js CHANGED
@@ -68,6 +68,7 @@ const VALID_RULES = [
68
68
  'trailing_paren',
69
69
  'trailing_whitespace',
70
70
  'type_check',
71
+ 'undeclared_setq',
71
72
  'unused_let_binding',
72
73
  'unused_local_fun',
73
74
  'unused_package_dep',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/lint",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "AutoLISP static analysis tool — parens, security, conventions, SBCL syntax validation",
5
5
  "keywords": [
6
6
  "CAD",