@atlisp/lint 0.2.22 → 0.2.25

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
@@ -102,7 +102,6 @@ npx @atlisp/lint --format-check
102
102
  | `redundant_nil_else` | warn | 风格 | if 中冗余 nil 分支 |
103
103
  | `single_arg_and_or` | warn | 风格 | 单参数 and/or 可简化 |
104
104
  | `double_not` | warn | 风格 | 双重 (not (not ...)) |
105
- | `multiple_setq` | warn | 风格 | 连续 setq 可合并 |
106
105
  | `setq_multiple` | warn | 风格 | 连续 setq 可合并 |
107
106
  | `setq_single_arg` | warn | 正确性 | setq 缺少值 |
108
107
  | `assoc_without_cdr` | warn | 正确性 | assoc 结果未用 cdr 提取 |
@@ -32,7 +32,6 @@
32
32
  "recursive_call": "warn",
33
33
  "variable_shadow": "warn",
34
34
  "unused_local_fun": "warn",
35
- "multiple_setq": "off",
36
35
  "redundant_quotes": "warn",
37
36
  "trailing_paren": "warn",
38
37
  "empty_comment": "warn",
@@ -90,7 +90,6 @@
90
90
  "mixed_indent": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
91
91
  "module_cycle": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "warn" },
92
92
  "module_registration": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "off" },
93
- "multiple_setq": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
94
93
  "namespace_header": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "off" },
95
94
  "no_return": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
96
95
  "nth_usage": { "type": "string", "enum": ["off", "info", "warn", "error"], "default": "info" },
@@ -179,7 +179,7 @@ const BUILTIN_ARITY = {
179
179
  acad_colordlg: { min: 1, max: 3 },
180
180
  // === ActiveX ===
181
181
  'vlax-invoke-method': { min: 2, max: -1 },
182
- 'vlax-get-property': { min: 2, max: 2 },
182
+ 'vlax-get-property': { min: 2, max: -1 },
183
183
  'vlax-put-property': { min: 3, max: 3 },
184
184
  'vlax-get-or-create-object': { min: 1, max: 2 },
185
185
  'vlax-create-object': { min: 1, max: 1 },
@@ -284,8 +284,12 @@ function checkNode(node, content, file, issues) {
284
284
  });
285
285
  }
286
286
  }
287
- for (const child of node.children) {
288
- checkNode(child, content, file, issues);
287
+ // Lambda lists may contain a bare `/` separator, which is not a function call
288
+ const lambdaListIdx = funcName === 'lambda' ? 1 : funcName === 'defun' || funcName === 'defun-q' ? 2 : -1;
289
+ for (let i = 0; i < node.children.length; i++) {
290
+ if (i === lambdaListIdx)
291
+ continue;
292
+ checkNode(node.children[i], content, file, issues);
289
293
  }
290
294
  }
291
295
  //# sourceMappingURL=builtin-arg-count.js.map
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkCommentedCode = checkCommentedCode;
4
4
  const locale_1 = require("../locale");
5
+ const utils_1 = require("../utils");
5
6
  const CODE_PATTERNS = [
6
7
  /^\s*\(defun\s/i,
7
8
  /^\s*\(setq\s/i,
@@ -22,17 +23,25 @@ const CODE_PATTERNS = [
22
23
  function checkCommentedCode(content, file) {
23
24
  const issues = [];
24
25
  const lines = content.split('\n');
26
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
25
27
  for (let i = 0; i < lines.length; i++) {
28
+ const lineNum = i + 1;
29
+ // Skip lines inside block comments
30
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
31
+ continue;
26
32
  const line = lines[i];
27
33
  const match = line.match(/^\s*;+([^;].*)$/);
28
34
  if (!match)
29
35
  continue;
36
+ // Skip block comment markers ;| and |;
30
37
  const afterSemicolon = match[1];
38
+ if (afterSemicolon.startsWith('|'))
39
+ continue;
31
40
  for (const pat of CODE_PATTERNS) {
32
41
  if (pat.test(afterSemicolon)) {
33
42
  issues.push({
34
43
  file,
35
- line: i + 1,
44
+ line: lineNum,
36
45
  severity: 'warn',
37
46
  rule: 'commented_code',
38
47
  message: (0, locale_1.t)('commented_code'),
@@ -2,18 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkEmptyComments = checkEmptyComments;
4
4
  const locale_1 = require("../locale");
5
+ const utils_1 = require("../utils");
5
6
  function checkEmptyComments(content, file) {
6
7
  const issues = [];
7
8
  const lines = content.split('\n');
9
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
8
10
  for (let i = 0; i < lines.length; i++) {
11
+ const lineNum = i + 1;
12
+ // Skip lines inside block comments
13
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
14
+ continue;
9
15
  const line = lines[i].trim();
10
16
  // Check: line is a comment with only whitespace after the ;
11
- if (line.startsWith(';')) {
17
+ // But skip block comment markers ;| and |;
18
+ if (line.startsWith(';') && !line.startsWith(';|') && !line.startsWith('|;')) {
12
19
  const afterSemicolon = line.slice(1);
13
20
  if (afterSemicolon.trim() === '') {
14
21
  issues.push({
15
22
  file,
16
- line: i + 1,
23
+ line: lineNum,
17
24
  severity: 'warn',
18
25
  rule: 'empty_comment',
19
26
  message: (0, locale_1.t)('empty_comment'),
@@ -11,15 +11,20 @@ function checkErrorHandling(content, file) {
11
11
  let defunStart = 0;
12
12
  let defunText = '';
13
13
  let hasErrorHandler = false;
14
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
14
15
  let runningDepth = 0;
15
16
  for (let i = 0; i < lines.length; i++) {
17
+ const lineNum = i + 1;
18
+ // Skip lines inside block comments
19
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
20
+ continue;
16
21
  const stripped = (0, utils_1.stripLine)(lines[i]);
17
22
  if (!inDefun) {
18
23
  const m = stripped.match(/\(defun\s+(\S+)/);
19
24
  if (m) {
20
25
  inDefun = true;
21
26
  defunName = m[1];
22
- defunStart = i + 1;
27
+ defunStart = lineNum;
23
28
  defunText = '';
24
29
  hasErrorHandler = false;
25
30
  runningDepth = 0;
@@ -7,7 +7,12 @@ function checkExtraParens(content, file) {
7
7
  const issues = [];
8
8
  const lines = content.split('\n');
9
9
  let runningDepth = 0;
10
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
10
11
  for (let i = 0; i < lines.length; i++) {
12
+ const lineNum = i + 1;
13
+ // Skip lines inside block comments
14
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
15
+ continue;
11
16
  const stripped = (0, utils_1.stripLine)(lines[i]);
12
17
  const closeOnly = stripped.trim();
13
18
  // Empty line resets nothing; just continue tracking depth
@@ -29,7 +34,7 @@ function checkExtraParens(content, file) {
29
34
  const extra = lineClose - runningDepth - lineOpen;
30
35
  issues.push({
31
36
  file,
32
- line: i + 1,
37
+ line: lineNum,
33
38
  severity: 'warn',
34
39
  rule: 'extra_parens',
35
40
  message: (0, locale_1.t)('extra_parens', extra),
@@ -7,7 +7,12 @@ function checkFunctionOrder(content, file) {
7
7
  const issues = [];
8
8
  const lines = content.split('\n');
9
9
  const defined = new Set();
10
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
10
11
  for (let i = 0; i < lines.length; i++) {
12
+ const lineNum = i + 1;
13
+ // Skip lines inside block comments
14
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
15
+ continue;
11
16
  const stripped = (0, utils_1.stripLine)(lines[i]);
12
17
  const defunM = stripped.match(/\(defun\s+(\S+)/);
13
18
  if (defunM) {
@@ -24,7 +29,7 @@ function checkFunctionOrder(content, file) {
24
29
  continue;
25
30
  if (!defined.has(name) && name.length > 1 && /^[a-z]/.test(name)) {
26
31
  defined.add(name);
27
- issues.push({ file, line: i + 1, severity: 'warn', rule: 'function_order', message: (0, locale_1.t)('function_order', name) });
32
+ issues.push({ file, line: lineNum, severity: 'warn', rule: 'function_order', message: (0, locale_1.t)('function_order', name) });
28
33
  }
29
34
  }
30
35
  }
@@ -6,7 +6,12 @@ const utils_1 = require("../utils");
6
6
  function checkMissingDoc(content, file) {
7
7
  const issues = [];
8
8
  const lines = content.split('\n');
9
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
9
10
  for (let i = 0; i < lines.length; i++) {
11
+ const lineNum = i + 1;
12
+ // Skip lines inside block comments
13
+ if ((0, utils_1.isInsideBlockComment)(lineNum, blockRanges))
14
+ continue;
10
15
  const stripped = (0, utils_1.stripLine)(lines[i]);
11
16
  const m = stripped.match(/\(defun\s+(\S+)/);
12
17
  if (!m)
@@ -18,10 +23,10 @@ function checkMissingDoc(content, file) {
18
23
  continue;
19
24
  if (name === 'T' || name === 'nil')
20
25
  continue;
21
- if (!hasDocString(lines, i)) {
26
+ if (!hasDocString(lines, i, blockRanges)) {
22
27
  issues.push({
23
28
  file,
24
- line: i + 1,
29
+ line: lineNum,
25
30
  severity: 'warn',
26
31
  rule: 'missing_doc',
27
32
  message: (0, locale_1.t)('missing_doc', name),
@@ -30,16 +35,30 @@ function checkMissingDoc(content, file) {
30
35
  }
31
36
  return issues;
32
37
  }
33
- function hasDocString(lines, defunLine) {
38
+ function hasDocString(lines, defunLine, blockRanges) {
34
39
  let depth = 0;
35
40
  let inArgs = false;
36
41
  let argsDone = false;
37
42
  let inStr = false;
38
43
  let foundExpr = false;
44
+ let inBlockComment = false;
39
45
  for (let i = defunLine; i < lines.length; i++) {
40
46
  const line = lines[i];
41
47
  for (let j = 0; j < line.length; j++) {
42
48
  const ch = line[j];
49
+ // Handle block comments
50
+ if (!inStr && !inBlockComment && ch === ';' && line[j + 1] === '|') {
51
+ inBlockComment = true;
52
+ j++; // skip |
53
+ continue;
54
+ }
55
+ if (inBlockComment) {
56
+ if (ch === '|' && line[j + 1] === ';') {
57
+ inBlockComment = false;
58
+ j++; // skip ;
59
+ }
60
+ continue;
61
+ }
43
62
  if (ch === ';' && !inStr)
44
63
  break;
45
64
  if (inStr) {
@@ -2,11 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkParens = checkParens;
4
4
  const locale_1 = require("../locale");
5
+ const utils_1 = require("../utils");
5
6
  function checkParens(content, file) {
6
7
  let openP = 0, closeP = 0;
7
8
  let inStr = false;
8
- const lines = content.split('\n');
9
- for (const line of lines) {
9
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
10
+ const cleaned = (0, utils_1.skipBlockComments)(content, blockRanges);
11
+ const lines = cleaned.split('\n');
12
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
13
+ const line = lines[lineIdx];
14
+ // Skip lines that are entirely within a block comment
15
+ if ((0, utils_1.isInsideBlockComment)(lineIdx + 1, blockRanges))
16
+ continue;
10
17
  let i = 0;
11
18
  while (i < line.length) {
12
19
  const ch = line[i];
@@ -2,39 +2,98 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkStringConcatInLoop = checkStringConcatInLoop;
4
4
  const locale_1 = require("../locale");
5
- function checkStringConcatInLoop(content, file) {
6
- const issues = [];
7
- const lines = content.split('\n');
5
+ function maskLine(line) {
6
+ let out = '';
7
+ let inString = false;
8
+ for (let k = 0; k < line.length; k++) {
9
+ const ch = line[k];
10
+ if (inString) {
11
+ if (ch === '\\') {
12
+ out += ' ';
13
+ k++;
14
+ }
15
+ else {
16
+ if (ch === '"')
17
+ inString = false;
18
+ out += ' ';
19
+ }
20
+ continue;
21
+ }
22
+ if (ch === '"') {
23
+ inString = true;
24
+ out += ' ';
25
+ continue;
26
+ }
27
+ if (ch === ';')
28
+ break;
29
+ out += ch;
30
+ }
31
+ return out;
32
+ }
33
+ function findLoops(lines) {
34
+ const loops = [];
8
35
  const re = /\((while|repeat|foreach)\s/g;
9
36
  for (let i = 0; i < lines.length; i++) {
37
+ const masked = maskLine(lines[i]);
10
38
  let match;
11
- while ((match = re.exec(lines[i])) !== null) {
12
- const name = match[1];
13
- let depth = 0;
14
- let started = false;
15
- for (let j = i; j < lines.length; j++) {
16
- for (let k = 0; k < lines[j].length; k++) {
17
- const ch = lines[j][k];
18
- if (ch === '(')
19
- depth++;
20
- else if (ch === ')')
21
- depth--;
39
+ while ((match = re.exec(masked)) !== null) {
40
+ loops.push({ name: match[1], line: i, col: match.index });
41
+ }
42
+ }
43
+ return loops;
44
+ }
45
+ // Find the line index where the form opening at (startLine, startCol) closes.
46
+ // Returns -1 if the form is never closed (unbalanced input).
47
+ function findFormEnd(lines, startLine, startCol) {
48
+ let depth = 0;
49
+ let inString = false;
50
+ for (let j = startLine; j < lines.length; j++) {
51
+ const line = lines[j];
52
+ const begin = j === startLine ? startCol : 0;
53
+ for (let k = begin; k < line.length; k++) {
54
+ const ch = line[k];
55
+ if (inString) {
56
+ if (ch === '\\') {
57
+ k++;
58
+ }
59
+ else if (ch === '"') {
60
+ inString = false;
22
61
  }
23
- if (!started && depth > 0)
24
- started = true;
25
- if (depth === 0 && started)
26
- break;
62
+ continue;
27
63
  }
28
- const loopBody = lines.slice(i, i + 20).join('\n');
29
- if (loopBody.includes('strcat') || loopBody.includes('strcat ')) {
30
- issues.push({
31
- file,
32
- line: i + 1,
33
- severity: 'info',
34
- rule: 'string_concat_loop',
35
- message: (0, locale_1.t)('string_concat_loop', name),
36
- });
64
+ if (ch === '"') {
65
+ inString = true;
66
+ continue;
37
67
  }
68
+ if (ch === ';')
69
+ break;
70
+ if (ch === '(')
71
+ depth++;
72
+ else if (ch === ')') {
73
+ depth--;
74
+ if (depth === 0)
75
+ return j;
76
+ }
77
+ }
78
+ }
79
+ return -1;
80
+ }
81
+ function checkStringConcatInLoop(content, file) {
82
+ const issues = [];
83
+ const lines = content.split('\n');
84
+ for (const loop of findLoops(lines)) {
85
+ const end = findFormEnd(lines, loop.line, loop.col);
86
+ if (end < 0)
87
+ continue;
88
+ const loopBody = lines.slice(loop.line, end + 1).join('\n');
89
+ if (loopBody.includes('(strcat')) {
90
+ issues.push({
91
+ file,
92
+ line: loop.line + 1,
93
+ severity: 'info',
94
+ rule: 'string_concat_loop',
95
+ message: (0, locale_1.t)('string_concat_loop', loop.name),
96
+ });
38
97
  }
39
98
  }
40
99
  return issues;
@@ -2,14 +2,31 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkTrailingParen = checkTrailingParen;
4
4
  const locale_1 = require("../locale");
5
+ const utils_1 = require("../utils");
5
6
  function checkTrailingParen(content, file) {
6
7
  // Count total parens to detect excess closing parens
7
8
  let openCount = 0;
8
9
  let closeCount = 0;
9
10
  let inStr = false;
11
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
10
12
  // We must parse strings accurately to avoid counting parens inside strings
13
+ // Also skip block comments
14
+ let inBlockComment = false;
11
15
  for (let i = 0; i < content.length; i++) {
12
16
  const ch = content[i];
17
+ // Handle block comments
18
+ if (!inStr && !inBlockComment && ch === ';' && content[i + 1] === '|') {
19
+ inBlockComment = true;
20
+ i++; // skip |
21
+ continue;
22
+ }
23
+ if (inBlockComment) {
24
+ if (ch === '|' && content[i + 1] === ';') {
25
+ inBlockComment = false;
26
+ i++; // skip ;
27
+ }
28
+ continue;
29
+ }
13
30
  if (ch === '"')
14
31
  inStr = !inStr;
15
32
  if (inStr) {
package/dist/config.js CHANGED
@@ -88,7 +88,6 @@ const DEFAULT_CONFIG = {
88
88
  recursive_call: 'warn',
89
89
  variable_shadow: 'warn',
90
90
  unused_local_fun: 'warn',
91
- multiple_setq: 'info',
92
91
  redundant_quotes: 'info',
93
92
  trailing_paren: 'warn',
94
93
  empty_comment: 'info',
package/dist/disable.js CHANGED
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseDisableComments = parseDisableComments;
4
4
  exports.isDisabled = isDisabled;
5
+ const utils_1 = require("./utils");
5
6
  function parseDisableComments(content, fileLine) {
6
7
  const map = new Map();
7
8
  const lines = content.split('\n');
9
+ const blockRanges = (0, utils_1.getBlockCommentRanges)(content);
8
10
  for (let i = 0; i < lines.length; i++) {
11
+ const lineNum = i + 1;
9
12
  const line = lines[i];
13
+ // Disable comments inside block comments are also valid
14
+ // (this is a design choice - allows disabling rules within commented sections)
10
15
  const disableFileM = line.match(/;\s*atlisp-lint:\s*disable-file=(.+)/i);
11
16
  if (disableFileM) {
12
17
  const rules = disableFileM[1]
@@ -26,7 +31,7 @@ function parseDisableComments(content, fileLine) {
26
31
  .split(',')
27
32
  .map(r => r.trim())
28
33
  .filter(Boolean);
29
- let targetLine = i + 1;
34
+ let targetLine = lineNum;
30
35
  if (fileLine !== undefined)
31
36
  targetLine = fileLine;
32
37
  for (const rule of rules) {
package/dist/formatter.js CHANGED
@@ -79,7 +79,7 @@ function runStandardFormat(lines, lineIdx, depth, _indentSize, rawLine, trimmed)
79
79
  if (trimmedStripped === '') {
80
80
  return {
81
81
  formattedLines: [' '.repeat(depth * _indentSize) + trimmed.trimStart()],
82
- newDepth: depth
82
+ newDepth: depth,
83
83
  };
84
84
  }
85
85
  const opens = (trimmedStripped.match(/\(/g) || []).length;
@@ -97,7 +97,7 @@ function runStandardFormat(lines, lineIdx, depth, _indentSize, rawLine, trimmed)
97
97
  }
98
98
  return {
99
99
  formattedLines: [' '.repeat(effectiveDepth * _indentSize) + trimmed.trimStart()],
100
- newDepth: depth + opens - closes
100
+ newDepth: depth + opens - closes,
101
101
  };
102
102
  }
103
103
  function formatCode(content, indentSize = 2) {
package/dist/locale.js CHANGED
@@ -42,7 +42,6 @@ const messages = {
42
42
  recursive_call: "Function '{0}' calls itself — possible infinite recursion",
43
43
  variable_shadow: "Variable '{0}' is set with setq inside a let that binds the same name",
44
44
  unused_local_fun: "Local function/variable '{0}' is defined in defun with / but never used",
45
- multiple_setq: 'Consecutive (setq ...) forms — consider merging into a single (setq ...)',
46
45
  redundant_quotes: "Redundant double quote — ''x can be simplified to 'x",
47
46
  trailing_paren: "Excess closing parenthesis: {0} extra ')' found",
48
47
  empty_comment: 'Empty comment line — contains only a semicolon with no text',
@@ -186,7 +185,6 @@ Options:
186
185
  recursive_call: "函数 '{0}' 调用了自身——可能存在无限递归",
187
186
  variable_shadow: "变量 '{0}' 在 let 绑定了相同名称后被 setq 赋值",
188
187
  unused_local_fun: "局部函数/变量 '{0}' 在 defun 的 / 后定义但从未使用",
189
- multiple_setq: '连续多个 (setq ...) ——建议合并为一个 (setq ...)',
190
188
  redundant_quotes: "冗余双引号 —— ''x 可以简化为 'x",
191
189
  trailing_paren: "多余闭合括号:发现 {0} 个额外的 ')'",
192
190
  empty_comment: '空注释行 —— 分号后没有内容',
package/dist/presets.js CHANGED
@@ -27,7 +27,6 @@ exports.PRESETS = {
27
27
  recursive_call: 'warn',
28
28
  variable_shadow: 'warn',
29
29
  unused_local_fun: 'warn',
30
- multiple_setq: 'info',
31
30
  redundant_quotes: 'info',
32
31
  trailing_paren: 'warn',
33
32
  empty_comment: 'info',
@@ -119,7 +118,6 @@ exports.PRESETS = {
119
118
  recursive_call: 'error',
120
119
  variable_shadow: 'error',
121
120
  unused_local_fun: 'error',
122
- multiple_setq: 'info',
123
121
  redundant_quotes: 'error',
124
122
  trailing_paren: 'error',
125
123
  empty_comment: 'error',
package/dist/rules.js CHANGED
@@ -122,7 +122,6 @@ exports.RULES = [
122
122
  description: '检测模块文件是否注册到 *modules*',
123
123
  category: '架构',
124
124
  },
125
- { name: 'multiple_setq', defaultSeverity: 'info', description: '检测多个连续 setq 可以合并', category: '风格' },
126
125
  {
127
126
  name: 'namespace_header',
128
127
  defaultSeverity: 'off',
package/dist/runner.js CHANGED
@@ -189,7 +189,6 @@ function runChecks(content, file, config) {
189
189
  'recursive_call',
190
190
  'variable_shadow',
191
191
  'unused_local_fun',
192
- 'multiple_setq',
193
192
  'redundant_quotes',
194
193
  'redundant_setq',
195
194
  'redundant_nil_else',
package/dist/utils.d.ts CHANGED
@@ -11,6 +11,29 @@ export declare function codeLines(content: string): Generator<{
11
11
  export declare function findMatchingParen(s: string, start: number): number;
12
12
  /** Extract the next s-expression form starting at position */
13
13
  export declare function extractForm(s: string, start: number): [string, number];
14
+ /**
15
+ * Get ranges of block comments (;| ... |;) in content.
16
+ * Returns array of {startLine, endLine} (1-indexed).
17
+ * startLine is the line containing ;|, endLine contains |;
18
+ */
19
+ export declare function getBlockCommentRanges(content: string): Array<{
20
+ startLine: number;
21
+ endLine: number;
22
+ }>;
23
+ /** Check if a line (1-indexed) is inside a block comment */
24
+ export declare function isInsideBlockComment(line: number, ranges: Array<{
25
+ startLine: number;
26
+ endLine: number;
27
+ }>): boolean;
28
+ /**
29
+ * Given content and block comment ranges, return content with block comments
30
+ * replaced by spaces (preserving line structure). Useful for checks that
31
+ * need to ignore block-commented code.
32
+ */
33
+ export declare function skipBlockComments(content: string, ranges: Array<{
34
+ startLine: number;
35
+ endLine: number;
36
+ }>): string;
14
37
  /** Extract string literals from a line */
15
38
  export declare function extractStrings(line: string): string[];
16
39
  //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js CHANGED
@@ -5,6 +5,9 @@ exports.stripLineRaw = stripLineRaw;
5
5
  exports.codeLines = codeLines;
6
6
  exports.findMatchingParen = findMatchingParen;
7
7
  exports.extractForm = extractForm;
8
+ exports.getBlockCommentRanges = getBlockCommentRanges;
9
+ exports.isInsideBlockComment = isInsideBlockComment;
10
+ exports.skipBlockComments = skipBlockComments;
8
11
  exports.extractStrings = extractStrings;
9
12
  function stripLineInternal(line, keepQuotes) {
10
13
  const out = [];
@@ -101,6 +104,98 @@ function extractForm(s, start) {
101
104
  const end = findMatchingParen(s, i);
102
105
  return [s.slice(i, end + 1), end + 1];
103
106
  }
107
+ /**
108
+ * Get ranges of block comments (;| ... |;) in content.
109
+ * Returns array of {startLine, endLine} (1-indexed).
110
+ * startLine is the line containing ;|, endLine contains |;
111
+ */
112
+ function getBlockCommentRanges(content) {
113
+ const ranges = [];
114
+ const lines = content.split('\n');
115
+ let inBlock = false;
116
+ let startLine = 0;
117
+ for (let i = 0; i < lines.length; i++) {
118
+ const line = lines[i];
119
+ if (!inBlock) {
120
+ // Look for ;| outside of strings and line comments
121
+ let inStr = false;
122
+ for (let j = 0; j < line.length; j++) {
123
+ const ch = line[j];
124
+ if (ch === ';' && !inStr) {
125
+ if (line[j + 1] === '|') {
126
+ inBlock = true;
127
+ startLine = i + 1; // 1-indexed
128
+ }
129
+ break; // Rest of line is comment or block comment start
130
+ }
131
+ if (ch === '"')
132
+ inStr = !inStr;
133
+ if (ch === '\\' && inStr)
134
+ j++; // skip escape
135
+ }
136
+ }
137
+ else {
138
+ // Look for |; to end block comment
139
+ for (let j = 0; j < line.length; j++) {
140
+ if (line[j] === '|' && line[j + 1] === ';') {
141
+ ranges.push({ startLine, endLine: i + 1 }); // 1-indexed
142
+ inBlock = false;
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ // Unterminated block comment
149
+ if (inBlock) {
150
+ ranges.push({ startLine, endLine: lines.length });
151
+ }
152
+ return ranges;
153
+ }
154
+ /** Check if a line (1-indexed) is inside a block comment */
155
+ function isInsideBlockComment(line, ranges) {
156
+ for (const r of ranges) {
157
+ if (line >= r.startLine && line <= r.endLine)
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ /**
163
+ * Given content and block comment ranges, return content with block comments
164
+ * replaced by spaces (preserving line structure). Useful for checks that
165
+ * need to ignore block-commented code.
166
+ */
167
+ function skipBlockComments(content, ranges) {
168
+ if (ranges.length === 0)
169
+ return content;
170
+ const lines = content.split('\n');
171
+ for (const r of ranges) {
172
+ for (let i = r.startLine - 1; i < Math.min(r.endLine, lines.length); i++) {
173
+ if (i === r.startLine - 1) {
174
+ // Keep content before ;|, replace rest with spaces
175
+ const idx = lines[i].indexOf(';|');
176
+ if (idx >= 0) {
177
+ lines[i] = lines[i].slice(0, idx) + ' '.repeat(lines[i].length - idx);
178
+ }
179
+ }
180
+ else if (i === r.endLine - 1 && r.endLine !== r.startLine) {
181
+ // Keep content after |;
182
+ const idx = lines[i].indexOf('|;');
183
+ if (idx >= 0) {
184
+ lines[i] = ' '.repeat(idx + 2) + lines[i].slice(idx + 2);
185
+ }
186
+ else {
187
+ // |; not found yet (unterminated), blank the whole line
188
+ lines[i] = ' '.repeat(lines[i].length);
189
+ }
190
+ }
191
+ else {
192
+ // Blank entire line
193
+ lines[i] = ' '.repeat(lines[i].length);
194
+ }
195
+ }
196
+ }
197
+ return lines.join('\n');
198
+ }
104
199
  /** Extract string literals from a line */
105
200
  function extractStrings(line) {
106
201
  const strings = [];
package/dist/validate.js CHANGED
@@ -39,7 +39,6 @@ const VALID_RULES = new Set([
39
39
  'missing_export',
40
40
  'mixed_indent',
41
41
  'module_registration',
42
- 'multiple_setq',
43
42
  'namespace_header',
44
43
  'no_return',
45
44
  'nth_usage',
@@ -285,31 +285,6 @@ function registerVisitorRules(visitor, state, checks, addIssue, config, file) {
285
285
  });
286
286
  }
287
287
  }
288
- if (checks['multiple_setq'] !== 'off') {
289
- visitor.on('*', node => {
290
- if ((0, parser_1.astIsList)(node)) {
291
- // Skip condition/branch positions (if/and/or/while/...) where setq
292
- // children are mutually exclusive or short-circuited — merging would break them.
293
- if ((0, shared_1.isNonSequentialParent)(node))
294
- return;
295
- let prevSetq = false;
296
- for (const child of node.children) {
297
- if ((0, parser_1.astIsList)(child, 'setq')) {
298
- if (prevSetq) {
299
- addIssue('multiple_setq', child.pos.line, (0, locale_1.t)('multiple_setq'));
300
- prevSetq = false;
301
- }
302
- else {
303
- prevSetq = true;
304
- }
305
- }
306
- else {
307
- prevSetq = false;
308
- }
309
- }
310
- }
311
- });
312
- }
313
288
  if (checks['recursive_call'] !== 'off') {
314
289
  visitor.on('defun', node => {
315
290
  if (!(0, parser_1.astIsList)(node))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/lint",
3
- "version": "0.2.22",
3
+ "version": "0.2.25",
4
4
  "description": "AutoLISP static analysis tool — parens, security, conventions, SBCL syntax validation",
5
5
  "keywords": [
6
6
  "CAD",