@atlisp/lint 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/atlisp-lint.schema.json +32 -2
  2. package/dist/cache.js +19 -9
  3. package/dist/checks/builtin-arg-count.d.ts +1 -0
  4. package/dist/checks/builtin-arg-count.js +5 -0
  5. package/dist/checks/dangerous-calls.js +1 -0
  6. package/dist/checks/dangling-defun.d.ts +7 -0
  7. package/dist/checks/dangling-defun.js +19 -0
  8. package/dist/checks/global-function-naming.js +159 -35
  9. package/dist/checks/missing-princ.js +2 -2
  10. package/dist/checks/nth-usage.d.ts +1 -0
  11. package/dist/checks/nth-usage.js +16 -1
  12. package/dist/checks/unused-package-dep.d.ts +1 -0
  13. package/dist/checks/unused-package-dep.js +53 -0
  14. package/dist/cli/collector.d.ts +7 -0
  15. package/dist/cli/collector.js +154 -0
  16. package/dist/cli/parser.d.ts +30 -0
  17. package/dist/cli/parser.js +119 -0
  18. package/dist/config.js +32 -6
  19. package/dist/disable.js +12 -0
  20. package/dist/fixes/index.d.ts +8 -0
  21. package/dist/fixes/index.js +200 -0
  22. package/dist/index.js +56 -236
  23. package/dist/locale.js +30 -2
  24. package/dist/presets.js +24 -0
  25. package/dist/project.js +182 -134
  26. package/dist/rules.d.ts +1 -0
  27. package/dist/rules.js +85 -0
  28. package/dist/runner.d.ts +2 -3
  29. package/dist/runner.js +46 -371
  30. package/dist/types.d.ts +6 -0
  31. package/dist/utils.d.ts +4 -0
  32. package/dist/utils.js +43 -0
  33. package/dist/validate.js +12 -0
  34. package/dist/visitor-runner.d.ts +28 -0
  35. package/dist/visitor-runner.js +115 -1
  36. package/dist/visitors/equal-nil.d.ts +8 -0
  37. package/dist/visitors/equal-nil.js +26 -0
  38. package/dist/visitors/index.d.ts +5 -0
  39. package/dist/visitors/index.js +14 -0
  40. package/dist/visitors/progn-in-cond.d.ts +8 -0
  41. package/dist/visitors/progn-in-cond.js +33 -0
  42. package/dist/visitors/redundant-and-or.d.ts +8 -0
  43. package/dist/visitors/redundant-and-or.js +27 -0
  44. package/dist/visitors/redundant-car-cdr.d.ts +8 -0
  45. package/dist/visitors/redundant-car-cdr.js +35 -0
  46. package/dist/visitors/types.d.ts +13 -0
  47. package/dist/visitors/types.js +72 -0
  48. package/package.json +1 -1
@@ -4,6 +4,7 @@ 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
+ const index_1 = require("./visitors/index");
7
8
  function runChecksWithVisitor(ast, file, config, disableMap) {
8
9
  const issues = [];
9
10
  const checks = config.checks;
@@ -17,6 +18,8 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
17
18
  defunScopes: [],
18
19
  letScopes: [],
19
20
  dangerousCalls: [],
21
+ ssgetCount: 0,
22
+ sssetCount: 0,
20
23
  };
21
24
  function addIssue(rule, line, message) {
22
25
  const severity = checks[rule];
@@ -435,6 +438,8 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
435
438
  dangerNames.add('startapp');
436
439
  if (dc.vl_registry_write !== 'off')
437
440
  dangerNames.add('vl-registry-write');
441
+ if (dc.eval !== 'off')
442
+ dangerNames.add('eval');
438
443
  if (dangerNames.size > 0) {
439
444
  visitor.on('*', node => {
440
445
  if (node.type === 'list' &&
@@ -695,6 +700,106 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
695
700
  }
696
701
  });
697
702
  }
703
+ // command_s — recommend (command-s ...) over (command ...)
704
+ if (checks['command_s'] !== 'off') {
705
+ visitor.on('command', node => {
706
+ if (node.children && node.children.length >= 2) {
707
+ const subCmd = node.children[1];
708
+ if (subCmd.type === 'string' && typeof subCmd.value === 'string' && subCmd.value === 'shell')
709
+ return;
710
+ addIssue('command_s', node.pos.line, (0, locale_1.t)('command_s'));
711
+ }
712
+ });
713
+ }
714
+ // zerop_usage — (= x 0) → (zerop x)
715
+ if (checks['zerop_usage'] !== 'off') {
716
+ const checkZero = (node) => {
717
+ if (node.children && node.children.length === 3) {
718
+ const a = node.children[1];
719
+ const b = node.children[2];
720
+ const extract = (n) => n.type === 'symbol' ? n.name || '' : n.type === 'number' ? String(n.value ?? '') : '';
721
+ if (a.type === 'number' && a.value === 0 && b.type === 'symbol') {
722
+ addIssue('zerop_usage', node.pos.line, (0, locale_1.t)('zerop_usage', extract(b)));
723
+ }
724
+ else if (b.type === 'number' && b.value === 0 && a.type === 'symbol') {
725
+ addIssue('zerop_usage', node.pos.line, (0, locale_1.t)('zerop_usage', extract(a)));
726
+ }
727
+ }
728
+ };
729
+ visitor.on('=', checkZero);
730
+ visitor.on('equal', checkZero);
731
+ }
732
+ // progn_in_body — redundant progn in while/repeat/foreach
733
+ if (checks['progn_in_body'] !== 'off') {
734
+ const checkPrognInBody = (node, formName, bodyStartIdx) => {
735
+ if (node.children && node.children.length > bodyStartIdx) {
736
+ const firstBody = node.children[bodyStartIdx];
737
+ if (firstBody.type === 'list' &&
738
+ firstBody.children &&
739
+ firstBody.children.length > 0 &&
740
+ firstBody.children[0].type === 'symbol' &&
741
+ firstBody.children[0].name === 'progn') {
742
+ addIssue('progn_in_body', firstBody.pos.line, (0, locale_1.t)('progn_in_body', formName));
743
+ }
744
+ }
745
+ };
746
+ visitor.on('while', node => checkPrognInBody(node, 'while', 2));
747
+ visitor.on('repeat', node => checkPrognInBody(node, 'repeat', 2));
748
+ visitor.on('foreach', node => checkPrognInBody(node, 'foreach', 3));
749
+ }
750
+ // if_progn_both — both branches of if use progn
751
+ if (checks['if_progn_both'] !== 'off') {
752
+ visitor.on('if', node => {
753
+ if (node.children && node.children.length === 4) {
754
+ const thenBranch = node.children[2];
755
+ const elseBranch = node.children[3];
756
+ const isProgn = (n) => n.type === 'list' &&
757
+ n.children !== undefined &&
758
+ n.children.length > 0 &&
759
+ n.children[0].type === 'symbol' &&
760
+ n.children[0].name === 'progn';
761
+ if (isProgn(thenBranch) && isProgn(elseBranch)) {
762
+ addIssue('if_progn_both', node.pos.line, (0, locale_1.t)('if_progn_both'));
763
+ }
764
+ }
765
+ });
766
+ }
767
+ // redundant_cond — single-clause cond
768
+ if (checks['redundant_cond'] !== 'off') {
769
+ visitor.on('cond', node => {
770
+ if (node.children && node.children.length === 2) {
771
+ const clause = node.children[1];
772
+ if (clause.type === 'list' && clause.children && clause.children.length === 2) {
773
+ const test = clause.children[0];
774
+ if (test.type === 'symbol' && test.name === 't')
775
+ return;
776
+ addIssue('redundant_cond', node.pos.line, (0, locale_1.t)('redundant_cond'));
777
+ }
778
+ }
779
+ });
780
+ }
781
+ // empty_branch_cond — cond clause with no body
782
+ if (checks['empty_branch_cond'] !== 'off') {
783
+ visitor.on('cond', node => {
784
+ if (!node.children)
785
+ return;
786
+ for (let i = 1; i < node.children.length; i++) {
787
+ const clause = node.children[i];
788
+ if (clause.type === 'list' && (!clause.children || clause.children.length < 2)) {
789
+ addIssue('empty_branch_cond', clause.pos.line, (0, locale_1.t)('empty_branch_cond'));
790
+ }
791
+ }
792
+ });
793
+ }
794
+ // selection_set_leak (data collection)
795
+ if (checks['selection_set_leak'] !== 'off') {
796
+ visitor.on('ssget', () => {
797
+ state.ssgetCount++;
798
+ });
799
+ visitor.on('ssset', () => {
800
+ state.sssetCount++;
801
+ });
802
+ }
698
803
  // promise_handler
699
804
  if (checks['promise_handler'] !== 'off') {
700
805
  visitor.on('*', node => {
@@ -732,6 +837,7 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
732
837
  }
733
838
  });
734
839
  }
840
+ (0, index_1.registerAll)(visitor, state, checks, addIssue);
735
841
  visitor.run(ast);
736
842
  // ===== POST-PROCESSING =====
737
843
  // open_without_close
@@ -754,12 +860,20 @@ function runChecksWithVisitor(ast, file, config, disableMap) {
754
860
  ? 'dangerous.startapp'
755
861
  : call.name === 'vl-registry-write'
756
862
  ? 'dangerous.vl_registry_write'
757
- : '';
863
+ : call.name === 'eval'
864
+ ? 'dangerous.eval'
865
+ : '';
758
866
  if (msgKey) {
759
867
  addIssue(call.name === 'vl-registry-write' ? 'vl_registry_write' : call.name, call.node.pos.line, (0, locale_1.t)(msgKey));
760
868
  }
761
869
  }
762
870
  }
871
+ // selection_set_leak
872
+ if (checks['selection_set_leak'] !== 'off') {
873
+ if (state.ssgetCount > state.sssetCount) {
874
+ addIssue('selection_set_leak', 1, (0, locale_1.t)('selection_set_leak', String(state.ssgetCount), String(state.sssetCount)));
875
+ }
876
+ }
763
877
  // unused_variable
764
878
  if (checks['unused_variable'] !== 'off') {
765
879
  for (const [name, def] of state.setqDefs) {
@@ -0,0 +1,8 @@
1
+ import { AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ import { AddIssueFn } from './types';
4
+ export declare const equalNilModule: {
5
+ name: string;
6
+ register(visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn): void;
7
+ };
8
+ //# sourceMappingURL=equal-nil.d.ts.map
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.equalNilModule = void 0;
4
+ const types_1 = require("./types");
5
+ exports.equalNilModule = {
6
+ name: 'equal_nil',
7
+ register(visitor, state, checks, addIssue) {
8
+ if (checks['equal_nil'] === 'off')
9
+ return;
10
+ const checkEqualNil = (node) => {
11
+ if (!node.children || node.children.length < 3)
12
+ return;
13
+ if ((0, types_1.isInQuote)(node))
14
+ return;
15
+ const a = node.children[1];
16
+ const b = node.children[2];
17
+ const isNilSym = (n) => n.type === 'symbol' && (n.name === 'nil' || n.name === 'NIL');
18
+ const varName = a.type === 'symbol' && isNilSym(b) ? a.name || '' : b.type === 'symbol' && isNilSym(a) ? b.name || '' : '';
19
+ if (varName) {
20
+ addIssue('equal_nil', node.pos.line, `(equal ${varName} nil) — use (null ${varName}) or (not ${varName})`);
21
+ }
22
+ };
23
+ visitor.on('equal', checkEqualNil);
24
+ },
25
+ };
26
+ //# sourceMappingURL=equal-nil.js.map
@@ -0,0 +1,5 @@
1
+ import { AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ import { AddIssueFn } from './types';
4
+ export declare function registerAll(visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn): void;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAll = registerAll;
4
+ const progn_in_cond_1 = require("./progn-in-cond");
5
+ const redundant_car_cdr_1 = require("./redundant-car-cdr");
6
+ const equal_nil_1 = require("./equal-nil");
7
+ const redundant_and_or_1 = require("./redundant-and-or");
8
+ const modules = [progn_in_cond_1.prognInCondModule, redundant_car_cdr_1.redundantCarCdrModule, equal_nil_1.equalNilModule, redundant_and_or_1.redundantAndOrModule];
9
+ function registerAll(visitor, state, checks, addIssue) {
10
+ for (const mod of modules) {
11
+ mod.register(visitor, state, checks, addIssue);
12
+ }
13
+ }
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ import { AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ import { AddIssueFn } from './types';
4
+ export declare const prognInCondModule: {
5
+ name: string;
6
+ register(visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn): void;
7
+ };
8
+ //# sourceMappingURL=progn-in-cond.d.ts.map
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prognInCondModule = void 0;
4
+ const types_1 = require("./types");
5
+ exports.prognInCondModule = {
6
+ name: 'progn_in_cond',
7
+ register(visitor, state, checks, addIssue) {
8
+ if (checks['progn_in_cond'] === 'off')
9
+ return;
10
+ visitor.on('cond', (node) => {
11
+ if (!node.children)
12
+ return;
13
+ for (let i = 1; i < node.children.length; i++) {
14
+ const clause = node.children[i];
15
+ if (clause.type !== 'list' || !clause.children || clause.children.length < 2)
16
+ continue;
17
+ for (let j = 1; j < clause.children.length; j++) {
18
+ const form = clause.children[j];
19
+ if (form.type === 'list' &&
20
+ form.children &&
21
+ form.children.length > 0 &&
22
+ form.children[0].type === 'symbol' &&
23
+ form.children[0].name === 'progn') {
24
+ if ((0, types_1.isInQuote)(form))
25
+ continue;
26
+ addIssue('progn_in_cond', form.pos.line, 'cond clause with progn — cond already allows multiple forms');
27
+ }
28
+ }
29
+ }
30
+ });
31
+ },
32
+ };
33
+ //# sourceMappingURL=progn-in-cond.js.map
@@ -0,0 +1,8 @@
1
+ import { AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ import { AddIssueFn } from './types';
4
+ export declare const redundantAndOrModule: {
5
+ name: string;
6
+ register(visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn): void;
7
+ };
8
+ //# sourceMappingURL=redundant-and-or.d.ts.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redundantAndOrModule = void 0;
4
+ const types_1 = require("./types");
5
+ exports.redundantAndOrModule = {
6
+ name: 'redundant_and_or',
7
+ register(visitor, state, checks, addIssue) {
8
+ if (checks['redundant_and_or'] === 'off')
9
+ return;
10
+ visitor.on('*', (node) => {
11
+ if (node.type !== 'list' || !node.children || node.children.length !== 1)
12
+ return;
13
+ const fn = node.children[0];
14
+ if (fn.type !== 'symbol')
15
+ return;
16
+ if ((0, types_1.isInQuote)(node))
17
+ return;
18
+ if (fn.name === 'and') {
19
+ addIssue('redundant_and_or', node.pos.line, '(and) always returns T — may be leftover code');
20
+ }
21
+ else if (fn.name === 'or') {
22
+ addIssue('redundant_and_or', node.pos.line, '(or) always returns nil — may be leftover code');
23
+ }
24
+ });
25
+ },
26
+ };
27
+ //# sourceMappingURL=redundant-and-or.js.map
@@ -0,0 +1,8 @@
1
+ import { AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ import { AddIssueFn } from './types';
4
+ export declare const redundantCarCdrModule: {
5
+ name: string;
6
+ register(visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn): void;
7
+ };
8
+ //# sourceMappingURL=redundant-car-cdr.d.ts.map
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redundantCarCdrModule = void 0;
4
+ const types_1 = require("./types");
5
+ const CAR_CDR_MAP = {
6
+ car: { car: 'caar', cdr: 'cadr' },
7
+ cdr: { car: 'cdar', cdr: 'cddr' },
8
+ };
9
+ exports.redundantCarCdrModule = {
10
+ name: 'redundant_car_cdr',
11
+ register(visitor, state, checks, addIssue) {
12
+ if (checks['redundant_car_cdr'] === 'off')
13
+ return;
14
+ visitor.on('*', (node) => {
15
+ if (node.type !== 'list' || !node.children || node.children.length !== 2)
16
+ return;
17
+ const outer = node.children[0];
18
+ if (outer.type !== 'symbol' || !outer.name)
19
+ return;
20
+ const inner = node.children[1];
21
+ if (inner.type !== 'list' || !inner.children || inner.children.length !== 2)
22
+ return;
23
+ const innerFn = inner.children[0];
24
+ if (innerFn.type !== 'symbol' || !innerFn.name)
25
+ return;
26
+ const replacement = CAR_CDR_MAP[outer.name]?.[innerFn.name];
27
+ if (!replacement)
28
+ return;
29
+ if ((0, types_1.isInQuote)(node))
30
+ return;
31
+ addIssue('redundant_car_cdr', node.pos.line, `(${outer.name} (${innerFn.name} x)) → (${replacement} x)`);
32
+ });
33
+ },
34
+ };
35
+ //# sourceMappingURL=redundant-car-cdr.js.map
@@ -0,0 +1,13 @@
1
+ import { AstNode, AstVisitor } from '@atlisp/parser';
2
+ import { CollectState } from '../visitor-runner';
3
+ export type AddIssueFn = (rule: string, line: number, message: string) => void;
4
+ export interface VisitorModule {
5
+ name: string;
6
+ register: (visitor: AstVisitor, state: CollectState, checks: Record<string, string>, addIssue: AddIssueFn) => void;
7
+ }
8
+ export declare function isInQuote(node: AstNode): boolean;
9
+ export declare function isLiteral(node: AstNode): boolean;
10
+ export declare function astEqual(a: AstNode, b: AstNode): boolean;
11
+ export declare function getCondKey(node: AstNode): string;
12
+ export declare function hasConditionalForm(body: AstNode[]): boolean;
13
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInQuote = isInQuote;
4
+ exports.isLiteral = isLiteral;
5
+ exports.astEqual = astEqual;
6
+ exports.getCondKey = getCondKey;
7
+ exports.hasConditionalForm = hasConditionalForm;
8
+ function isInQuote(node) {
9
+ let p = node.parent;
10
+ while (p) {
11
+ if (p.type === 'list' &&
12
+ p.children &&
13
+ p.children.length > 0 &&
14
+ p.children[0].type === 'symbol' &&
15
+ (p.children[0].name === 'quote' || p.children[0].name === 'function')) {
16
+ return true;
17
+ }
18
+ p = p.parent;
19
+ }
20
+ return false;
21
+ }
22
+ function isLiteral(node) {
23
+ if (node.type === 'symbol' && (node.name === 'T' || node.name === 't' || node.name === 'nil'))
24
+ return true;
25
+ if (node.type === 'number')
26
+ return true;
27
+ return false;
28
+ }
29
+ function astEqual(a, b) {
30
+ if (a.type !== b.type)
31
+ return false;
32
+ if (a.type === 'symbol')
33
+ return a.name === b.name;
34
+ if (a.type === 'string')
35
+ return a.value === b.value;
36
+ if (a.type === 'number')
37
+ return a.value === b.value;
38
+ if (a.type === 'list') {
39
+ const ac = a.children || [];
40
+ const bc = b.children || [];
41
+ if (ac.length !== bc.length)
42
+ return false;
43
+ for (let i = 0; i < ac.length; i++) {
44
+ if (!astEqual(ac[i], bc[i]))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ function getCondKey(node) {
52
+ if (node.type === 'symbol' || node.type === 'keyword')
53
+ return node.name || '';
54
+ if (node.type === 'string' || node.type === 'number')
55
+ return String(node.value);
56
+ if (node.type === 'list' && node.children) {
57
+ return '(' + node.children.map(c => getCondKey(c)).join(' ') + ')';
58
+ }
59
+ return '';
60
+ }
61
+ function hasConditionalForm(body) {
62
+ for (const form of body) {
63
+ if (form.type === 'list' && form.children && form.children.length > 0 && form.children[0].type === 'symbol') {
64
+ const name = form.children[0].name;
65
+ if (name === 'if' || name === 'cond' || name === 'when' || name === 'unless') {
66
+ return true;
67
+ }
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlisp/lint",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "AutoLISP static analysis tool — parens, security, conventions, SBCL syntax validation",
5
5
  "keywords": [
6
6
  "CAD",