distyll 0.1.0
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/CONTRIBUTING.md +159 -0
- package/POSTMORTEM.json +60 -0
- package/README.md +218 -0
- package/SETUP.md +79 -0
- package/action.yml +37 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +115 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ci.d.ts +7 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +101 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/diff.d.ts +10 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +95 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fingerprint.d.ts +2 -0
- package/dist/commands/fingerprint.d.ts.map +1 -0
- package/dist/commands/fingerprint.js +77 -0
- package/dist/commands/fingerprint.js.map +1 -0
- package/dist/commands/hook.d.ts +3 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +110 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +75 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +100 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +133 -0
- package/dist/errors.js.map +1 -0
- package/dist/fingerprint/analyzer.d.ts +3 -0
- package/dist/fingerprint/analyzer.d.ts.map +1 -0
- package/dist/fingerprint/analyzer.js +230 -0
- package/dist/fingerprint/analyzer.js.map +1 -0
- package/dist/fingerprint/comparator.d.ts +4 -0
- package/dist/fingerprint/comparator.d.ts.map +1 -0
- package/dist/fingerprint/comparator.js +78 -0
- package/dist/fingerprint/comparator.js.map +1 -0
- package/dist/fingerprint/profile.d.ts +5 -0
- package/dist/fingerprint/profile.d.ts.map +1 -0
- package/dist/fingerprint/profile.js +68 -0
- package/dist/fingerprint/profile.js.map +1 -0
- package/dist/fixes/index.d.ts +12 -0
- package/dist/fixes/index.d.ts.map +1 -0
- package/dist/fixes/index.js +42 -0
- package/dist/fixes/index.js.map +1 -0
- package/dist/fixes/single-use-wrapper.d.ts +8 -0
- package/dist/fixes/single-use-wrapper.d.ts.map +1 -0
- package/dist/fixes/single-use-wrapper.js +54 -0
- package/dist/fixes/single-use-wrapper.js.map +1 -0
- package/dist/fixes/unnecessary-try-catch.d.ts +8 -0
- package/dist/fixes/unnecessary-try-catch.d.ts.map +1 -0
- package/dist/fixes/unnecessary-try-catch.js +37 -0
- package/dist/fixes/unnecessary-try-catch.js.map +1 -0
- package/dist/fixes/unused-imports.d.ts +7 -0
- package/dist/fixes/unused-imports.d.ts.map +1 -0
- package/dist/fixes/unused-imports.js +41 -0
- package/dist/fixes/unused-imports.js.map +1 -0
- package/dist/fixes/verbose-comments.d.ts +7 -0
- package/dist/fixes/verbose-comments.d.ts.map +1 -0
- package/dist/fixes/verbose-comments.js +29 -0
- package/dist/fixes/verbose-comments.js.map +1 -0
- package/dist/formatter.d.ts +4 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +72 -0
- package/dist/formatter.js.map +1 -0
- package/dist/git.d.ts +22 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +130 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/languages/index.d.ts +8 -0
- package/dist/languages/index.d.ts.map +1 -0
- package/dist/languages/index.js +50 -0
- package/dist/languages/index.js.map +1 -0
- package/dist/languages/javascript.d.ts +6 -0
- package/dist/languages/javascript.d.ts.map +1 -0
- package/dist/languages/javascript.js +39 -0
- package/dist/languages/javascript.js.map +1 -0
- package/dist/languages/python.d.ts +6 -0
- package/dist/languages/python.d.ts.map +1 -0
- package/dist/languages/python.js +50 -0
- package/dist/languages/python.js.map +1 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +55 -0
- package/dist/parser.js.map +1 -0
- package/dist/reporters/github.d.ts +4 -0
- package/dist/reporters/github.d.ts.map +1 -0
- package/dist/reporters/github.js +70 -0
- package/dist/reporters/github.js.map +1 -0
- package/dist/reporters/terminal.d.ts +4 -0
- package/dist/reporters/terminal.d.ts.map +1 -0
- package/dist/reporters/terminal.js +59 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/rules/dead-code-paths.d.ts +3 -0
- package/dist/rules/dead-code-paths.d.ts.map +1 -0
- package/dist/rules/dead-code-paths.js +57 -0
- package/dist/rules/dead-code-paths.js.map +1 -0
- package/dist/rules/excessive-comments.d.ts +3 -0
- package/dist/rules/excessive-comments.d.ts.map +1 -0
- package/dist/rules/excessive-comments.js +86 -0
- package/dist/rules/excessive-comments.js.map +1 -0
- package/dist/rules/hallucinated-imports.d.ts +3 -0
- package/dist/rules/hallucinated-imports.d.ts.map +1 -0
- package/dist/rules/hallucinated-imports.js +228 -0
- package/dist/rules/hallucinated-imports.js.map +1 -0
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +34 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/magic-values.d.ts +3 -0
- package/dist/rules/magic-values.d.ts.map +1 -0
- package/dist/rules/magic-values.js +168 -0
- package/dist/rules/magic-values.js.map +1 -0
- package/dist/rules/near-duplicate-functions.d.ts +3 -0
- package/dist/rules/near-duplicate-functions.d.ts.map +1 -0
- package/dist/rules/near-duplicate-functions.js +78 -0
- package/dist/rules/near-duplicate-functions.js.map +1 -0
- package/dist/rules/over-defensive-nulls.d.ts +3 -0
- package/dist/rules/over-defensive-nulls.d.ts.map +1 -0
- package/dist/rules/over-defensive-nulls.js +129 -0
- package/dist/rules/over-defensive-nulls.js.map +1 -0
- package/dist/rules/redundant-else-return.d.ts +3 -0
- package/dist/rules/redundant-else-return.d.ts.map +1 -0
- package/dist/rules/redundant-else-return.js +57 -0
- package/dist/rules/redundant-else-return.js.map +1 -0
- package/dist/rules/single-option-object.d.ts +3 -0
- package/dist/rules/single-option-object.d.ts.map +1 -0
- package/dist/rules/single-option-object.js +88 -0
- package/dist/rules/single-option-object.js.map +1 -0
- package/dist/rules/single-use-wrapper.d.ts +3 -0
- package/dist/rules/single-use-wrapper.d.ts.map +1 -0
- package/dist/rules/single-use-wrapper.js +172 -0
- package/dist/rules/single-use-wrapper.js.map +1 -0
- package/dist/rules/unnecessary-try-catch.d.ts +3 -0
- package/dist/rules/unnecessary-try-catch.d.ts.map +1 -0
- package/dist/rules/unnecessary-try-catch.js +116 -0
- package/dist/rules/unnecessary-try-catch.js.map +1 -0
- package/dist/rules/unused-imports.d.ts +3 -0
- package/dist/rules/unused-imports.d.ts.map +1 -0
- package/dist/rules/unused-imports.js +103 -0
- package/dist/rules/unused-imports.js.map +1 -0
- package/dist/rules/verbose-comments.d.ts +3 -0
- package/dist/rules/verbose-comments.d.ts.map +1 -0
- package/dist/rules/verbose-comments.js +100 -0
- package/dist/rules/verbose-comments.js.map +1 -0
- package/dist/scanner.d.ts +11 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +196 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scorer.d.ts +3 -0
- package/dist/scorer.d.ts.map +1 -0
- package/dist/scorer.js +23 -0
- package/dist/scorer.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/hn_post.md +13 -0
- package/marketing/COMPETITIVE_ANALYSIS.md +62 -0
- package/marketing/EMAIL_ANNOUNCEMENT.md +91 -0
- package/marketing/LANDING_PAGE_COPY.md +123 -0
- package/marketing/LAUNCH_POST.md +68 -0
- package/marketing/PRODUCT_HUNT.md +39 -0
- package/marketing/TWITTER_THREAD.md +70 -0
- package/package.json +44 -0
- package/producthunt.md +52 -0
- package/reddit_post.md +39 -0
- package/site/favicon.svg +10 -0
- package/site/index.html +281 -0
- package/site/script.js +82 -0
- package/site/style.css +516 -0
- package/src/cache.ts +114 -0
- package/src/cli.ts +169 -0
- package/src/commands/ci.ts +111 -0
- package/src/commands/diff.ts +108 -0
- package/src/commands/fingerprint.ts +47 -0
- package/src/commands/hook.ts +85 -0
- package/src/commands/init.ts +42 -0
- package/src/config.ts +75 -0
- package/src/errors.ts +105 -0
- package/src/fingerprint/analyzer.ts +214 -0
- package/src/fingerprint/comparator.ts +93 -0
- package/src/fingerprint/profile.ts +32 -0
- package/src/fixes/index.ts +58 -0
- package/src/fixes/single-use-wrapper.ts +60 -0
- package/src/fixes/unnecessary-try-catch.ts +43 -0
- package/src/fixes/unused-imports.ts +53 -0
- package/src/fixes/verbose-comments.ts +35 -0
- package/src/formatter.ts +79 -0
- package/src/git.ts +115 -0
- package/src/index.ts +15 -0
- package/src/languages/index.ts +50 -0
- package/src/languages/javascript.ts +36 -0
- package/src/languages/python.ts +47 -0
- package/src/parser.ts +52 -0
- package/src/reporters/github.ts +75 -0
- package/src/reporters/terminal.ts +67 -0
- package/src/rules/dead-code-paths.ts +62 -0
- package/src/rules/excessive-comments.ts +94 -0
- package/src/rules/hallucinated-imports.ts +195 -0
- package/src/rules/index.ts +32 -0
- package/src/rules/magic-values.ts +167 -0
- package/src/rules/near-duplicate-functions.ts +89 -0
- package/src/rules/over-defensive-nulls.ts +137 -0
- package/src/rules/redundant-else-return.ts +61 -0
- package/src/rules/single-option-object.ts +97 -0
- package/src/rules/single-use-wrapper.ts +184 -0
- package/src/rules/unnecessary-try-catch.ts +121 -0
- package/src/rules/unused-imports.ts +115 -0
- package/src/rules/verbose-comments.ts +105 -0
- package/src/scanner.ts +184 -0
- package/src/scorer.ts +26 -0
- package/src/types.ts +70 -0
- package/tests/commands/diff.test.ts +107 -0
- package/tests/config.test.ts +69 -0
- package/tests/e2e.test.ts +163 -0
- package/tests/edge-cases.test.ts +167 -0
- package/tests/fingerprint/analyzer.test.ts +131 -0
- package/tests/fixes/unnecessary-try-catch.test.ts +62 -0
- package/tests/git.test.ts +79 -0
- package/tests/rules/hallucinated-imports.test.ts +59 -0
- package/tests/rules/near-duplicate-functions.test.ts +90 -0
- package/tests/rules/unnecessary-try-catch.test.ts +81 -0
- package/tests/scanner.test.ts +88 -0
- package/tsconfig.json +20 -0
- package/twitter_thread.md +46 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import { walkTree, findNodesMulti } from '../parser';
|
|
3
|
+
import type { Finding, Rule } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects patterns where code checks for null/undefined on values that
|
|
7
|
+
* were just assigned or are non-nullable by context. Focuses on clearly
|
|
8
|
+
* unnecessary checks to minimize false positives.
|
|
9
|
+
*
|
|
10
|
+
* Patterns detected:
|
|
11
|
+
* - Null check immediately after non-nullable assignment: `const x = 5; if (x != null) ...`
|
|
12
|
+
* - Double null checks: `if (x != null) { if (x != null) ... }`
|
|
13
|
+
* - Typeof check on known values: `const x = []; if (typeof x !== 'undefined') ...`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const LITERAL_TYPES = new Set([
|
|
17
|
+
'number', 'string', 'true', 'false', 'template_string',
|
|
18
|
+
'array', 'object', 'regex',
|
|
19
|
+
// Python
|
|
20
|
+
'integer', 'float', 'list', 'dictionary', 'set', 'tuple',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const NULL_CHECK_PATTERNS = new Set([
|
|
24
|
+
'!= null', '!== null', '!= undefined', '!== undefined',
|
|
25
|
+
'!= None', 'is not None',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function isNullCheck(node: Parser.SyntaxNode): boolean {
|
|
29
|
+
const text = node.text.replace(/\s+/g, ' ').trim();
|
|
30
|
+
for (const pattern of NULL_CHECK_PATTERNS) {
|
|
31
|
+
if (text.includes(pattern)) return true;
|
|
32
|
+
}
|
|
33
|
+
// typeof x !== 'undefined'
|
|
34
|
+
if (text.includes('typeof') && text.includes('undefined')) return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLiteralAssignment(node: Parser.SyntaxNode): boolean {
|
|
39
|
+
// Check if the right side of an assignment is a literal
|
|
40
|
+
const value = node.childForFieldName('value') ?? node.childForFieldName('right');
|
|
41
|
+
if (!value) return false;
|
|
42
|
+
return LITERAL_TYPES.has(value.type);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAssignedName(node: Parser.SyntaxNode): string | null {
|
|
46
|
+
const nameNode = node.childForFieldName('name') ?? node.childForFieldName('left');
|
|
47
|
+
if (!nameNode) return null;
|
|
48
|
+
return nameNode.text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const overDefensiveNulls: Rule = {
|
|
52
|
+
name: 'over-defensive-nulls',
|
|
53
|
+
description: 'Flags null/undefined checks on values that are clearly non-nullable',
|
|
54
|
+
severity: 'info',
|
|
55
|
+
|
|
56
|
+
check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
|
|
57
|
+
const findings: Finding[] = [];
|
|
58
|
+
|
|
59
|
+
// Strategy: find variable declarations with literal values, then check if
|
|
60
|
+
// the very next sibling is a null check on that variable
|
|
61
|
+
const declarations = findNodesMulti(tree.rootNode, [
|
|
62
|
+
'lexical_declaration', 'variable_declaration',
|
|
63
|
+
// Python
|
|
64
|
+
'assignment',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
for (const decl of declarations) {
|
|
68
|
+
let assignedName: string | null = null;
|
|
69
|
+
|
|
70
|
+
// For JS/TS: const x = literal
|
|
71
|
+
if (decl.type === 'lexical_declaration' || decl.type === 'variable_declaration') {
|
|
72
|
+
for (const child of decl.namedChildren) {
|
|
73
|
+
if (child.type === 'variable_declarator' && isLiteralAssignment(child)) {
|
|
74
|
+
assignedName = getAssignedName(child);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// For Python: x = literal
|
|
79
|
+
if (decl.type === 'assignment' && isLiteralAssignment(decl)) {
|
|
80
|
+
assignedName = getAssignedName(decl);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!assignedName) continue;
|
|
84
|
+
|
|
85
|
+
// Check the next sibling statement for a null check on this variable
|
|
86
|
+
const nextSibling = decl.nextNamedSibling;
|
|
87
|
+
if (!nextSibling) continue;
|
|
88
|
+
|
|
89
|
+
if (nextSibling.type === 'if_statement' || nextSibling.type === 'expression_statement') {
|
|
90
|
+
const condition = nextSibling.childForFieldName('condition') ?? nextSibling;
|
|
91
|
+
if (isNullCheck(condition) && condition.text.includes(assignedName)) {
|
|
92
|
+
findings.push({
|
|
93
|
+
file: filePath,
|
|
94
|
+
line: nextSibling.startPosition.row + 1,
|
|
95
|
+
column: nextSibling.startPosition.column + 1,
|
|
96
|
+
endLine: nextSibling.endPosition.row + 1,
|
|
97
|
+
endColumn: nextSibling.endPosition.column + 1,
|
|
98
|
+
rule: 'over-defensive-nulls',
|
|
99
|
+
severity: 'info',
|
|
100
|
+
message: `Null check on '${assignedName}' is unnecessary — it was just assigned a non-nullable value on line ${decl.startPosition.row + 1}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Detect nested duplicate null checks
|
|
107
|
+
const ifStatements = findNodesMulti(tree.rootNode, ['if_statement']);
|
|
108
|
+
for (const ifStmt of ifStatements) {
|
|
109
|
+
const condition = ifStmt.childForFieldName('condition');
|
|
110
|
+
if (!condition || !isNullCheck(condition)) continue;
|
|
111
|
+
|
|
112
|
+
const consequence = ifStmt.childForFieldName('consequence') ?? ifStmt.childForFieldName('body');
|
|
113
|
+
if (!consequence) continue;
|
|
114
|
+
|
|
115
|
+
// Look for nested if with the same null check
|
|
116
|
+
walkTree(consequence, (inner) => {
|
|
117
|
+
if (inner.type === 'if_statement' && inner !== ifStmt) {
|
|
118
|
+
const innerCond = inner.childForFieldName('condition');
|
|
119
|
+
if (innerCond && isNullCheck(innerCond) && innerCond.text === condition.text) {
|
|
120
|
+
findings.push({
|
|
121
|
+
file: filePath,
|
|
122
|
+
line: inner.startPosition.row + 1,
|
|
123
|
+
column: inner.startPosition.column + 1,
|
|
124
|
+
endLine: inner.endPosition.row + 1,
|
|
125
|
+
endColumn: inner.endPosition.column + 1,
|
|
126
|
+
rule: 'over-defensive-nulls',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
message: `Duplicate null check — '${innerCond.text}' is already checked on line ${ifStmt.startPosition.row + 1}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return findings;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import { findNodes } from '../parser';
|
|
3
|
+
import type { Finding, Rule } from '../types';
|
|
4
|
+
|
|
5
|
+
function blockEndsWithReturn(block: Parser.SyntaxNode): boolean {
|
|
6
|
+
const children = block.namedChildren;
|
|
7
|
+
if (children.length === 0) return false;
|
|
8
|
+
const last = children[children.length - 1];
|
|
9
|
+
return last.type === 'return_statement' || last.type === 'throw_statement';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const redundantElseReturn: Rule = {
|
|
13
|
+
name: 'redundant-else-return',
|
|
14
|
+
description: 'Flags else blocks after if blocks that end with return/throw',
|
|
15
|
+
severity: 'info',
|
|
16
|
+
|
|
17
|
+
check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
|
|
18
|
+
const findings: Finding[] = [];
|
|
19
|
+
const ifStatements = findNodes(tree.rootNode, 'if_statement');
|
|
20
|
+
|
|
21
|
+
for (const ifNode of ifStatements) {
|
|
22
|
+
const consequence = ifNode.childForFieldName('consequence');
|
|
23
|
+
const alternative = ifNode.childForFieldName('alternative');
|
|
24
|
+
|
|
25
|
+
if (!consequence || !alternative) continue;
|
|
26
|
+
|
|
27
|
+
// Skip if the alternative is another if statement (else if chains are fine)
|
|
28
|
+
if (alternative.type === 'if_statement') continue;
|
|
29
|
+
|
|
30
|
+
// For else_clause, check the inner block
|
|
31
|
+
let altBlock = alternative;
|
|
32
|
+
if (alternative.type === 'else_clause') {
|
|
33
|
+
const inner = alternative.namedChildren[0];
|
|
34
|
+
if (!inner || inner.type === 'if_statement') continue;
|
|
35
|
+
altBlock = inner;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if the if-block ends with return or throw
|
|
39
|
+
let ifBlock = consequence;
|
|
40
|
+
if (consequence.type === 'statement_block') {
|
|
41
|
+
ifBlock = consequence;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!blockEndsWithReturn(ifBlock)) continue;
|
|
45
|
+
|
|
46
|
+
// The else block is redundant — the code inside could be un-indented
|
|
47
|
+
findings.push({
|
|
48
|
+
file: filePath,
|
|
49
|
+
line: alternative.startPosition.row + 1,
|
|
50
|
+
column: alternative.startPosition.column + 1,
|
|
51
|
+
endLine: alternative.endPosition.row + 1,
|
|
52
|
+
endColumn: alternative.endPosition.column + 1,
|
|
53
|
+
rule: 'redundant-else-return',
|
|
54
|
+
severity: 'info',
|
|
55
|
+
message: 'else block is unnecessary after return/throw — un-indent and remove the else',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return findings;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import { findNodesMulti } from '../parser';
|
|
3
|
+
import type { Finding, Rule } from '../types';
|
|
4
|
+
|
|
5
|
+
function getDestructuredParams(node: Parser.SyntaxNode): Parser.SyntaxNode[] {
|
|
6
|
+
const params = node.childForFieldName('parameters');
|
|
7
|
+
if (!params) return [];
|
|
8
|
+
|
|
9
|
+
return params.namedChildren.filter((p) => {
|
|
10
|
+
// Destructured object parameter: function({ foo }) {}
|
|
11
|
+
if (p.type === 'object_pattern') return true;
|
|
12
|
+
// TypeScript: function({ foo }: Options) {}
|
|
13
|
+
if (p.type === 'required_parameter' || p.type === 'optional_parameter') {
|
|
14
|
+
const pattern = p.childForFieldName('pattern');
|
|
15
|
+
if (pattern?.type === 'object_pattern') return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getObjectPatternPropertyCount(node: Parser.SyntaxNode): number {
|
|
22
|
+
let objectPattern = node;
|
|
23
|
+
if (node.type === 'required_parameter' || node.type === 'optional_parameter') {
|
|
24
|
+
const pattern = node.childForFieldName('pattern');
|
|
25
|
+
if (pattern?.type === 'object_pattern') objectPattern = pattern;
|
|
26
|
+
else return 0;
|
|
27
|
+
}
|
|
28
|
+
if (objectPattern.type !== 'object_pattern') return 0;
|
|
29
|
+
|
|
30
|
+
// Count actual properties (not rest elements)
|
|
31
|
+
return objectPattern.namedChildren.filter(
|
|
32
|
+
(c) =>
|
|
33
|
+
c.type === 'shorthand_property_identifier_pattern' ||
|
|
34
|
+
c.type === 'pair_pattern' ||
|
|
35
|
+
c.type === 'object_assignment_pattern' ||
|
|
36
|
+
c.type === 'assignment_pattern'
|
|
37
|
+
).length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getFunctionName(node: Parser.SyntaxNode): string | null {
|
|
41
|
+
if (node.type === 'function_declaration' || node.type === 'method_definition') {
|
|
42
|
+
return node.childForFieldName('name')?.text ?? null;
|
|
43
|
+
}
|
|
44
|
+
if (node.parent?.type === 'variable_declarator') {
|
|
45
|
+
return node.parent.childForFieldName('name')?.text ?? null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const singleOptionObject: Rule = {
|
|
51
|
+
name: 'single-option-object',
|
|
52
|
+
description: 'Flags function parameters that destructure an object with only one property',
|
|
53
|
+
severity: 'info',
|
|
54
|
+
|
|
55
|
+
check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
|
|
56
|
+
const findings: Finding[] = [];
|
|
57
|
+
|
|
58
|
+
const functions = findNodesMulti(tree.rootNode, [
|
|
59
|
+
'function_declaration',
|
|
60
|
+
'arrow_function',
|
|
61
|
+
'function',
|
|
62
|
+
'method_definition',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
for (const fn of functions) {
|
|
66
|
+
const destructuredParams = getDestructuredParams(fn);
|
|
67
|
+
|
|
68
|
+
for (const param of destructuredParams) {
|
|
69
|
+
const propCount = getObjectPatternPropertyCount(param);
|
|
70
|
+
|
|
71
|
+
if (propCount === 1) {
|
|
72
|
+
// Skip if function has only this one parameter and it's a callback
|
|
73
|
+
const parent = fn.parent;
|
|
74
|
+
if (parent?.type === 'arguments' || parent?.type === 'call_expression') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const name = getFunctionName(fn);
|
|
79
|
+
const label = name ? `Function '${name}'` : 'Function';
|
|
80
|
+
|
|
81
|
+
findings.push({
|
|
82
|
+
file: filePath,
|
|
83
|
+
line: param.startPosition.row + 1,
|
|
84
|
+
column: param.startPosition.column + 1,
|
|
85
|
+
endLine: param.endPosition.row + 1,
|
|
86
|
+
endColumn: param.endPosition.column + 1,
|
|
87
|
+
rule: 'single-option-object',
|
|
88
|
+
severity: 'info',
|
|
89
|
+
message: `${label} destructures an options object with only 1 property — use a plain parameter instead`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return findings;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import { findNodesMulti } from '../parser';
|
|
3
|
+
import type { Finding, Rule } from '../types';
|
|
4
|
+
|
|
5
|
+
function getFunctionBody(node: Parser.SyntaxNode): Parser.SyntaxNode | null {
|
|
6
|
+
return node.childForFieldName('body');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getFunctionName(node: Parser.SyntaxNode): string | null {
|
|
10
|
+
if (node.type === 'function_declaration' || node.type === 'method_definition') {
|
|
11
|
+
const nameNode = node.childForFieldName('name');
|
|
12
|
+
return nameNode?.text ?? null;
|
|
13
|
+
}
|
|
14
|
+
if (node.type === 'variable_declarator') {
|
|
15
|
+
const nameNode = node.childForFieldName('name');
|
|
16
|
+
return nameNode?.text ?? null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStatements(body: Parser.SyntaxNode): Parser.SyntaxNode[] {
|
|
22
|
+
if (body.type === 'statement_block') {
|
|
23
|
+
return body.namedChildren;
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const COMPLEX_ARG_TYPES = new Set([
|
|
29
|
+
'arrow_function', 'function', 'object', 'array',
|
|
30
|
+
'template_string', 'binary_expression', 'new_expression',
|
|
31
|
+
'ternary_expression', 'conditional_expression',
|
|
32
|
+
'unary_expression', 'update_expression', 'assignment_expression',
|
|
33
|
+
'augmented_assignment_expression', 'yield_expression',
|
|
34
|
+
'spread_element', 'regex', 'class',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function isSimpleNode(node: Parser.SyntaxNode): boolean {
|
|
38
|
+
if (node.type === 'identifier' || node.type === 'this' ||
|
|
39
|
+
node.type === 'string' || node.type === 'number' ||
|
|
40
|
+
node.type === 'true' || node.type === 'false' ||
|
|
41
|
+
node.type === 'null' || node.type === 'undefined') {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Simple member_expression like a.b (not computed a[b])
|
|
45
|
+
if (node.type === 'member_expression') {
|
|
46
|
+
const computed = node.children.some(c => c.type === '[');
|
|
47
|
+
return !computed;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasOnlySimpleArguments(callExpr: Parser.SyntaxNode): boolean {
|
|
53
|
+
// Check if callee involves complex expressions (e.g. new Foo().method())
|
|
54
|
+
const callee = callExpr.childForFieldName('function');
|
|
55
|
+
if (callee?.type === 'member_expression') {
|
|
56
|
+
const object = callee.childForFieldName('object');
|
|
57
|
+
if (object && COMPLEX_ARG_TYPES.has(object.type)) return false;
|
|
58
|
+
// Also check for chained calls with complex args: foo(complex).bar(simple)
|
|
59
|
+
if (object?.type === 'call_expression') {
|
|
60
|
+
if (!hasOnlySimpleArguments(object)) return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const args = callExpr.childForFieldName('arguments');
|
|
65
|
+
if (!args) return true;
|
|
66
|
+
|
|
67
|
+
for (const arg of args.namedChildren) {
|
|
68
|
+
if (isSimpleNode(arg)) continue;
|
|
69
|
+
// Known complex types
|
|
70
|
+
if (COMPLEX_ARG_TYPES.has(arg.type)) return false;
|
|
71
|
+
// Anything else unexpected — be conservative, treat as complex
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractCallExpression(node: Parser.SyntaxNode): Parser.SyntaxNode | null {
|
|
78
|
+
if (node.type === 'call_expression') return node;
|
|
79
|
+
// Parenthesized expression wrapping a call
|
|
80
|
+
if (node.type === 'parenthesized_expression' && node.namedChildren.length === 1) {
|
|
81
|
+
return extractCallExpression(node.namedChildren[0]);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isSimpleDelegatingFunction(body: Parser.SyntaxNode): boolean {
|
|
87
|
+
// For arrow functions with expression body (no braces)
|
|
88
|
+
if (body.type === 'call_expression') {
|
|
89
|
+
return hasOnlySimpleArguments(body);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stmts = getStatements(body);
|
|
93
|
+
if (stmts.length !== 1) return false;
|
|
94
|
+
|
|
95
|
+
const stmt = stmts[0];
|
|
96
|
+
|
|
97
|
+
// Single return statement with a function call
|
|
98
|
+
if (stmt.type === 'return_statement') {
|
|
99
|
+
const returnValue = stmt.namedChildren[0];
|
|
100
|
+
if (!returnValue) return false;
|
|
101
|
+
const call = extractCallExpression(returnValue);
|
|
102
|
+
if (!call) return false;
|
|
103
|
+
return hasOnlySimpleArguments(call);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Single expression statement that is a function call (void wrapper)
|
|
107
|
+
if (stmt.type === 'expression_statement') {
|
|
108
|
+
const expr = stmt.namedChildren[0];
|
|
109
|
+
if (!expr) return false;
|
|
110
|
+
const call = extractCallExpression(expr);
|
|
111
|
+
if (!call) return false;
|
|
112
|
+
return hasOnlySimpleArguments(call);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getParameterCount(node: Parser.SyntaxNode): number {
|
|
119
|
+
const params = node.childForFieldName('parameters');
|
|
120
|
+
if (!params) return 0;
|
|
121
|
+
return params.namedChildren.filter(c =>
|
|
122
|
+
c.type === 'identifier' ||
|
|
123
|
+
c.type === 'required_parameter' ||
|
|
124
|
+
c.type === 'optional_parameter' ||
|
|
125
|
+
c.type === 'rest_parameter' ||
|
|
126
|
+
c.type === 'assignment_pattern'
|
|
127
|
+
).length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const singleUseWrapper: Rule = {
|
|
131
|
+
name: 'single-use-wrapper',
|
|
132
|
+
description: 'Flags functions that merely delegate to another function without adding logic',
|
|
133
|
+
severity: 'warning',
|
|
134
|
+
|
|
135
|
+
check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
|
|
136
|
+
const findings: Finding[] = [];
|
|
137
|
+
|
|
138
|
+
const functionNodes = findNodesMulti(tree.rootNode, [
|
|
139
|
+
'function_declaration',
|
|
140
|
+
'arrow_function',
|
|
141
|
+
'function',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
for (const fn of functionNodes) {
|
|
145
|
+
const body = getFunctionBody(fn);
|
|
146
|
+
if (!body) continue;
|
|
147
|
+
|
|
148
|
+
if (!isSimpleDelegatingFunction(body)) continue;
|
|
149
|
+
|
|
150
|
+
// Skip if function has more than 3 parameters (likely an intentional adapter)
|
|
151
|
+
if (getParameterCount(fn) > 3) continue;
|
|
152
|
+
|
|
153
|
+
// Skip anonymous functions passed as callbacks — those are idiomatic
|
|
154
|
+
if (fn.type === 'arrow_function' || fn.type === 'function') {
|
|
155
|
+
const parent = fn.parent;
|
|
156
|
+
if (parent?.type === 'arguments' || parent?.type === 'call_expression' ||
|
|
157
|
+
parent?.type === 'array' || parent?.type === 'pair') {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try to get the function name for the message
|
|
163
|
+
let name = getFunctionName(fn);
|
|
164
|
+
if (!name && fn.parent?.type === 'variable_declarator') {
|
|
165
|
+
name = getFunctionName(fn.parent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const label = name ? `Function '${name}'` : 'Function';
|
|
169
|
+
|
|
170
|
+
findings.push({
|
|
171
|
+
file: filePath,
|
|
172
|
+
line: fn.startPosition.row + 1,
|
|
173
|
+
column: fn.startPosition.column + 1,
|
|
174
|
+
endLine: fn.endPosition.row + 1,
|
|
175
|
+
endColumn: fn.endPosition.column + 1,
|
|
176
|
+
rule: 'single-use-wrapper',
|
|
177
|
+
severity: 'warning',
|
|
178
|
+
message: `${label} only delegates to another function — consider inlining it`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return findings;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type Parser from 'tree-sitter';
|
|
2
|
+
import { findNodes, walkTree } from '../parser';
|
|
3
|
+
import type { Finding, Rule } from '../types';
|
|
4
|
+
|
|
5
|
+
const THROWING_CALL_INDICATORS = new Set([
|
|
6
|
+
'fetch', 'require', 'import', 'eval',
|
|
7
|
+
'JSON.parse', 'JSON.stringify',
|
|
8
|
+
'parseInt', 'parseFloat',
|
|
9
|
+
'decodeURI', 'decodeURIComponent',
|
|
10
|
+
'atob', 'btoa',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function isAwaitExpression(node: Parser.SyntaxNode): boolean {
|
|
14
|
+
let found = false;
|
|
15
|
+
walkTree(node, (n) => {
|
|
16
|
+
if (n.type === 'await_expression') found = true;
|
|
17
|
+
});
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function containsThrowStatement(node: Parser.SyntaxNode): boolean {
|
|
22
|
+
let found = false;
|
|
23
|
+
walkTree(node, (n) => {
|
|
24
|
+
if (n.type === 'throw_statement') found = true;
|
|
25
|
+
});
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function containsFunctionCall(node: Parser.SyntaxNode): boolean {
|
|
30
|
+
let found = false;
|
|
31
|
+
walkTree(node, (n) => {
|
|
32
|
+
if (n.type === 'call_expression') found = true;
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPropertyAccess(node: Parser.SyntaxNode): boolean {
|
|
38
|
+
let found = false;
|
|
39
|
+
walkTree(node, (n) => {
|
|
40
|
+
if (n.type === 'member_expression' || n.type === 'subscript_expression') found = true;
|
|
41
|
+
});
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function catchBlockOnlyRethrows(catchClause: Parser.SyntaxNode): boolean {
|
|
46
|
+
const body = catchClause.childForFieldName('body');
|
|
47
|
+
if (!body) return false;
|
|
48
|
+
|
|
49
|
+
const statements = body.namedChildren;
|
|
50
|
+
if (statements.length !== 1) return false;
|
|
51
|
+
|
|
52
|
+
const stmt = statements[0];
|
|
53
|
+
if (stmt.type !== 'throw_statement') return false;
|
|
54
|
+
|
|
55
|
+
const thrown = stmt.namedChildren[0];
|
|
56
|
+
if (!thrown) return false;
|
|
57
|
+
|
|
58
|
+
const catchParam = catchClause.childForFieldName('parameter');
|
|
59
|
+
if (!catchParam) return false;
|
|
60
|
+
|
|
61
|
+
return thrown.text === catchParam.text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const unnecessaryTryCatch: Rule = {
|
|
65
|
+
name: 'unnecessary-try-catch',
|
|
66
|
+
description: 'Flags try-catch blocks that wrap code unlikely to throw',
|
|
67
|
+
severity: 'warning',
|
|
68
|
+
|
|
69
|
+
check(tree: Parser.Tree, source: string, filePath: string): Finding[] {
|
|
70
|
+
const findings: Finding[] = [];
|
|
71
|
+
const tryStatements = findNodes(tree.rootNode, 'try_statement');
|
|
72
|
+
|
|
73
|
+
for (const tryNode of tryStatements) {
|
|
74
|
+
const body = tryNode.childForFieldName('body');
|
|
75
|
+
const handler = tryNode.childForFieldName('handler');
|
|
76
|
+
if (!body || !handler) continue;
|
|
77
|
+
|
|
78
|
+
// If catch block only rethrows, that's always useless
|
|
79
|
+
if (catchBlockOnlyRethrows(handler)) {
|
|
80
|
+
findings.push({
|
|
81
|
+
file: filePath,
|
|
82
|
+
line: tryNode.startPosition.row + 1,
|
|
83
|
+
column: tryNode.startPosition.column + 1,
|
|
84
|
+
endLine: tryNode.endPosition.row + 1,
|
|
85
|
+
endColumn: tryNode.endPosition.column + 1,
|
|
86
|
+
rule: 'unnecessary-try-catch',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
message: 'try-catch block catches an error only to rethrow it unchanged',
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Skip if the try body contains await (async operations can throw)
|
|
94
|
+
if (isAwaitExpression(body)) continue;
|
|
95
|
+
|
|
96
|
+
// Skip if try body contains throw statements
|
|
97
|
+
if (containsThrowStatement(body)) continue;
|
|
98
|
+
|
|
99
|
+
// Skip if try body contains function calls (they might throw)
|
|
100
|
+
if (containsFunctionCall(body)) continue;
|
|
101
|
+
|
|
102
|
+
// Skip if try body contains property access (might throw on null/undefined)
|
|
103
|
+
if (isPropertyAccess(body)) continue;
|
|
104
|
+
|
|
105
|
+
// If we get here, the try body only contains assignments, returns of
|
|
106
|
+
// literals/variables, or other purely synchronous non-throwing ops
|
|
107
|
+
findings.push({
|
|
108
|
+
file: filePath,
|
|
109
|
+
line: tryNode.startPosition.row + 1,
|
|
110
|
+
column: tryNode.startPosition.column + 1,
|
|
111
|
+
endLine: tryNode.endPosition.row + 1,
|
|
112
|
+
endColumn: tryNode.endPosition.column + 1,
|
|
113
|
+
rule: 'unnecessary-try-catch',
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
message: 'try-catch wraps code that is unlikely to throw — consider removing the try-catch',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return findings;
|
|
120
|
+
},
|
|
121
|
+
};
|