clean-code-tools 1.0.1

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 (77) hide show
  1. package/README.md +66 -0
  2. package/configs/eslint.clean-code.recommended.mjs +211 -0
  3. package/configs/python.clean-code.pyproject.toml +143 -0
  4. package/data/clean-code-patterns.jsonl +264 -0
  5. package/data/vector-record.schema.json +77 -0
  6. package/docs/README.md +29 -0
  7. package/docs/eslint-custom-rules.md +74 -0
  8. package/docs/eslint-recommended-config.md +87 -0
  9. package/docs/fastmcp-local-server.md +104 -0
  10. package/docs/publishing.md +125 -0
  11. package/docs/python-lint-recommended-config.md +57 -0
  12. package/docs/python-pylint-custom-rules.md +77 -0
  13. package/docs/semantic-weaviate.md +80 -0
  14. package/docs/static-trigger-semantic-review.md +97 -0
  15. package/evals/clean-code-retrieval.jsonl +13 -0
  16. package/ops/dev/weaviate/README.md +34 -0
  17. package/ops/dev/weaviate/compose.yaml +34 -0
  18. package/ops/dev/weaviate/smoke.sh +28 -0
  19. package/package.json +96 -0
  20. package/pyproject.toml +303 -0
  21. package/sample-apps/README.md +40 -0
  22. package/sample-apps/python-app/pyproject.toml +113 -0
  23. package/sample-apps/python-app/src/clean_pricing.py +10 -0
  24. package/sample-apps/python-app/src/smelly_pricing.py +8 -0
  25. package/sample-apps/ts-backend/eslint.config.mjs +3 -0
  26. package/sample-apps/ts-backend/package.json +18 -0
  27. package/sample-apps/ts-backend/src/clean-handler.ts +19 -0
  28. package/sample-apps/ts-backend/src/smelly-handler.ts +29 -0
  29. package/sample-apps/ts-backend/tsconfig.json +9 -0
  30. package/sample-apps/ts-frontend/eslint.config.mjs +3 -0
  31. package/sample-apps/ts-frontend/package.json +18 -0
  32. package/sample-apps/ts-frontend/src/CleanWidget.tsx +18 -0
  33. package/sample-apps/ts-frontend/src/SmellyWidget.tsx +27 -0
  34. package/sample-apps/ts-frontend/tsconfig.json +10 -0
  35. package/scripts/_mcp_app.py +21 -0
  36. package/scripts/check_clean_code_review_candidates.py +302 -0
  37. package/scripts/check_fastmcp_server.py +106 -0
  38. package/scripts/check_packages.py +137 -0
  39. package/scripts/check_python_config.py +130 -0
  40. package/scripts/check_repo_python_lint.py +46 -0
  41. package/scripts/check_retrieval_evals.py +132 -0
  42. package/scripts/check_sample_apps.py +169 -0
  43. package/scripts/check_semantic_search_tooling.py +102 -0
  44. package/scripts/clean_code_eslint_triggers.py +272 -0
  45. package/scripts/clean_code_mcp_server.py +7 -0
  46. package/scripts/clean_code_python_triggers.py +318 -0
  47. package/scripts/clean_code_review_candidates.py +291 -0
  48. package/scripts/clean_code_review_io.py +36 -0
  49. package/scripts/clean_code_review_models.py +43 -0
  50. package/scripts/clean_code_semantic.py +27 -0
  51. package/scripts/set_package_versions.py +82 -0
  52. package/scripts/weaviate_ingest_clean_code.py +44 -0
  53. package/scripts/weaviate_search_clean_code.py +51 -0
  54. package/skills/clean-code-mcp-reviewer/SKILL.md +209 -0
  55. package/skills/clean-code-mcp-reviewer/evals/evals.json +30 -0
  56. package/src/js/eslint-plugin-clean-code.mjs +758 -0
  57. package/src/python/clean_code_tools_pylint/__init__.py +14 -0
  58. package/src/python/clean_code_tools_pylint/ast_checker.py +122 -0
  59. package/src/python/clean_code_tools_pylint/comments.py +83 -0
  60. package/src/python/clean_code_tools_pylint/helpers.py +196 -0
  61. package/src/python/mcp_server/__init__.py +1 -0
  62. package/src/python/mcp_server/corpus.py +160 -0
  63. package/src/python/mcp_server/markdown.py +126 -0
  64. package/src/python/mcp_server/models.py +73 -0
  65. package/src/python/mcp_server/ranking.py +125 -0
  66. package/src/python/mcp_server/ranking_scoring.py +232 -0
  67. package/src/python/mcp_server/semantic.py +192 -0
  68. package/src/python/mcp_server/server.py +235 -0
  69. package/src/python/mcp_server/server_payloads.py +83 -0
  70. package/src/python/mcp_server/text.py +104 -0
  71. package/src/python/mcp_server/utils/__init__.py +1 -0
  72. package/src/python/mcp_server/utils/httpx_loader.py +14 -0
  73. package/src/python/mcp_server/utils/increment.py +7 -0
  74. package/src/python/mcp_server/utils/sha256_text.py +8 -0
  75. package/src/python/mcp_server/utils/unique_strings.py +15 -0
  76. package/src/python/mcp_server/weaviate.py +182 -0
  77. package/uv.lock +2012 -0
@@ -0,0 +1,758 @@
1
+ const DEFAULT_TODO_PATTERN = "^(TODO|FIXME|XXX)\\([A-Z][A-Z0-9]+-\\d+\\):\\s+\\S";
2
+ const DEFAULT_SELECTOR_PARAM_NAMES = [
3
+ "flag",
4
+ "mode",
5
+ "option",
6
+ "type",
7
+ "kind",
8
+ "variant",
9
+ "selector",
10
+ "enabled",
11
+ "disabled",
12
+ "dryRun",
13
+ "verbose",
14
+ "silent",
15
+ "force",
16
+ "skip",
17
+ "include",
18
+ "exclude",
19
+ ];
20
+ const DEFAULT_MUTATOR_METHODS = [
21
+ "add",
22
+ "append",
23
+ "clear",
24
+ "copyWithin",
25
+ "delete",
26
+ "fill",
27
+ "pop",
28
+ "push",
29
+ "reverse",
30
+ "set",
31
+ "shift",
32
+ "sort",
33
+ "splice",
34
+ "unshift",
35
+ ];
36
+ const DEFAULT_LITERAL_CALL_ALLOWLIST = [
37
+ "BigInt",
38
+ "Boolean",
39
+ "Date",
40
+ "Error",
41
+ "Number",
42
+ "RegExp",
43
+ "String",
44
+ "Symbol",
45
+ "console.debug",
46
+ "console.error",
47
+ "console.info",
48
+ "console.log",
49
+ "console.warn",
50
+ "expect",
51
+ "it",
52
+ "test",
53
+ ];
54
+
55
+ function createRule({ name, meta, create }) {
56
+ return {
57
+ meta: {
58
+ docs: {
59
+ url: `https://github.com/local/clean-code-tools/blob/main/docs/eslint-custom-rules.md#clean-code${name}`,
60
+ ...meta.docs,
61
+ },
62
+ ...meta,
63
+ },
64
+ create,
65
+ };
66
+ }
67
+
68
+ function getSourceCode(context) {
69
+ return context.sourceCode ?? context.getSourceCode();
70
+ }
71
+
72
+ function cleanCommentText(comment) {
73
+ return comment.value.trim().replace(/^\*+\s?/gm, "").trim();
74
+ }
75
+
76
+ function normalizeWords(value) {
77
+ return value
78
+ .replace(/[_$]+/gu, " ")
79
+ .replace(/([a-z])([A-Z])/gu, "$1 $2")
80
+ .toLowerCase()
81
+ .match(/[a-z][a-z0-9]+/gu) ?? [];
82
+ }
83
+
84
+ function getNodeTextWords(sourceCode, node) {
85
+ return normalizeWords(sourceCode.getText(node));
86
+ }
87
+
88
+ function getIdentifierName(node) {
89
+ if (!node) {
90
+ return undefined;
91
+ }
92
+ if (node.type === "Identifier") {
93
+ return node.name;
94
+ }
95
+ if (node.type === "PrivateIdentifier") {
96
+ return node.name;
97
+ }
98
+ if (node.type === "ThisExpression") {
99
+ return "this";
100
+ }
101
+ if (node.type === "MemberExpression" || node.type === "OptionalMemberExpression") {
102
+ return getIdentifierName(node.property);
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function getCalleeName(callee) {
108
+ if (!callee) {
109
+ return undefined;
110
+ }
111
+ if (callee.type === "Identifier") {
112
+ return callee.name;
113
+ }
114
+ if (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") {
115
+ const objectName = getCalleeName(callee.object);
116
+ const propertyName = getIdentifierName(callee.property);
117
+ return objectName && propertyName ? `${objectName}.${propertyName}` : propertyName;
118
+ }
119
+ return undefined;
120
+ }
121
+
122
+ function unwrapExpression(node) {
123
+ let current = node;
124
+ while (
125
+ current &&
126
+ ["ChainExpression", "TSAsExpression", "TSTypeAssertion", "TSNonNullExpression"].includes(current.type)
127
+ ) {
128
+ current = current.expression;
129
+ }
130
+ return current;
131
+ }
132
+
133
+ function isBooleanLiteral(node) {
134
+ const unwrapped = unwrapExpression(node);
135
+ return unwrapped?.type === "Literal" && typeof unwrapped.value === "boolean";
136
+ }
137
+
138
+ function isStringOrNumberLiteral(node) {
139
+ const unwrapped = unwrapExpression(node);
140
+ return (
141
+ unwrapped?.type === "Literal" &&
142
+ (typeof unwrapped.value === "string" || typeof unwrapped.value === "number")
143
+ );
144
+ }
145
+
146
+ function collectPatternIdentifiers(node) {
147
+ const identifiers = [];
148
+
149
+ function collect(current) {
150
+ if (!current) {
151
+ return;
152
+ }
153
+ switch (current.type) {
154
+ case "Identifier":
155
+ identifiers.push(current.name);
156
+ break;
157
+ case "AssignmentPattern":
158
+ collect(current.left);
159
+ break;
160
+ case "ArrayPattern":
161
+ for (const element of current.elements) {
162
+ collect(element);
163
+ }
164
+ break;
165
+ case "ObjectPattern":
166
+ for (const property of current.properties) {
167
+ collect(property.value ?? property.argument);
168
+ }
169
+ break;
170
+ case "RestElement":
171
+ collect(current.argument);
172
+ break;
173
+ }
174
+ }
175
+
176
+ collect(node);
177
+ return identifiers;
178
+ }
179
+
180
+ function getRootIdentifierName(node) {
181
+ const unwrapped = unwrapExpression(node);
182
+ if (!unwrapped) {
183
+ return undefined;
184
+ }
185
+ if (unwrapped.type === "Identifier") {
186
+ return unwrapped.name;
187
+ }
188
+ if (unwrapped.type === "MemberExpression" || unwrapped.type === "OptionalMemberExpression") {
189
+ return getRootIdentifierName(unwrapped.object);
190
+ }
191
+ return undefined;
192
+ }
193
+
194
+ function firstLineAfterComment(sourceCode, comment) {
195
+ const line = comment.loc.end.line + 1;
196
+ const lines = sourceCode.lines ?? sourceCode.getText().split(/\r?\n/u);
197
+ return lines[line - 1]?.trim() ?? "";
198
+ }
199
+
200
+ function isLikelyCodeComment(text) {
201
+ const trimmed = text.trim();
202
+ if (trimmed.length < 4) {
203
+ return false;
204
+ }
205
+ const codePatterns = [
206
+ /\b(await|const|let|var|function|class|interface|type|enum|return|throw|if|for|while|switch|import|export)\b/u,
207
+ /(?:^|\s)[\w$.]+\s*\([^)]*\)\s*;?$/u,
208
+ /=>/u,
209
+ /[{}]/u,
210
+ /^\s*<\/?[A-Za-z][^>]*>\s*$/u,
211
+ ];
212
+ return codePatterns.some((pattern) => pattern.test(trimmed));
213
+ }
214
+
215
+ function isSeparatorComment(text) {
216
+ const compact = text.replace(/\s+/gu, "");
217
+ return compact.length >= 8 && /^[-=*_/#]+$/u.test(compact);
218
+ }
219
+
220
+ function isBylineComment(text) {
221
+ return /\b(author|created by|written by|modified by|last modified|since)\b/iu.test(text);
222
+ }
223
+
224
+ function isDateOnlyComment(text) {
225
+ return /\b(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}|\d{1,2}[-/]\d{1,2}[-/]\d{2,4})\b/u.test(text);
226
+ }
227
+
228
+ function isClosingBraceComment(sourceCode, comment) {
229
+ const text = sourceCode.getText();
230
+ const lineStart = text.lastIndexOf("\n", comment.range[0] - 1) + 1;
231
+ const before = text.slice(lineStart, comment.range[0]);
232
+ return /\}\s*$/u.test(before);
233
+ }
234
+
235
+ function literalLooksLikePolicy(value) {
236
+ if (typeof value === "number") {
237
+ return ![-1, 0, 1].includes(value);
238
+ }
239
+ if (typeof value !== "string") {
240
+ return false;
241
+ }
242
+ if (value.length < 2) {
243
+ return false;
244
+ }
245
+ return (
246
+ /^[A-Z][A-Z0-9_]+$/u.test(value) ||
247
+ /^\d{4}-\d{2}-\d{2}$/u.test(value) ||
248
+ /(?:^|[_\s-])(active|approved|cancelled|canceled|draft|failed|paid|pending|rejected|retry|suspended)(?:$|[_\s-])/iu.test(
249
+ value,
250
+ )
251
+ );
252
+ }
253
+
254
+ function isAllowedLiteralContext(node, callAllowlist) {
255
+ let current = node.parent;
256
+ while (current) {
257
+ if (current.type === "ImportDeclaration" || current.type === "ExportNamedDeclaration") {
258
+ return true;
259
+ }
260
+ if (current.type === "Property" && current.key === node) {
261
+ return true;
262
+ }
263
+ if (current.type === "VariableDeclarator" && current.id?.type === "Identifier" && /^[A-Z][A-Z0-9_]+$/u.test(current.id.name)) {
264
+ return true;
265
+ }
266
+ if (current.type === "CallExpression" || current.type === "NewExpression") {
267
+ const calleeName = getCalleeName(current.callee);
268
+ return callAllowlist.includes(calleeName);
269
+ }
270
+ current = current.parent;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ function isTypeOnlyLiteral(node) {
276
+ let current = node.parent;
277
+ while (current) {
278
+ if (
279
+ current.type === "TSLiteralType" ||
280
+ current.type === "TSTypeAliasDeclaration" ||
281
+ current.type === "TSInterfaceDeclaration" ||
282
+ current.type === "TSEnumDeclaration" ||
283
+ current.type === "TSTypeAnnotation" ||
284
+ current.type === "TSTypeReference" ||
285
+ current.type === "TSUnionType" ||
286
+ current.type === "TSIntersectionType"
287
+ ) {
288
+ return true;
289
+ }
290
+ current = current.parent;
291
+ }
292
+ return false;
293
+ }
294
+
295
+ function isBooleanTypeAnnotation(node) {
296
+ const annotation = node?.typeAnnotation;
297
+ return annotation?.type === "TSBooleanKeyword" || annotation?.typeAnnotation?.type === "TSBooleanKeyword";
298
+ }
299
+
300
+ function nameLooksLikeSelector(name, configuredNames) {
301
+ const lowerName = name.toLowerCase();
302
+ return configuredNames.some((configuredName) => lowerName.includes(configuredName.toLowerCase()));
303
+ }
304
+
305
+ function isMemberDepthTooDeep(node, maxDepth) {
306
+ let depth = 0;
307
+ let current = unwrapExpression(node);
308
+ while (current?.type === "MemberExpression" || current?.type === "OptionalMemberExpression") {
309
+ depth += 1;
310
+ current = unwrapExpression(current.object);
311
+ }
312
+ return depth > maxDepth;
313
+ }
314
+
315
+ const todoFormat = createRule({
316
+ name: "/todo-format",
317
+ meta: {
318
+ type: "suggestion",
319
+ docs: {
320
+ description: "Require TODO, FIXME, and XXX comments to include an owner or issue identifier.",
321
+ },
322
+ schema: [
323
+ {
324
+ type: "object",
325
+ additionalProperties: false,
326
+ properties: {
327
+ pattern: { type: "string" },
328
+ },
329
+ },
330
+ ],
331
+ messages: {
332
+ invalidTodo:
333
+ "TODO/FIXME comments should include an owner or issue ID, for example TODO(PROJ-123): remove fallback.",
334
+ },
335
+ },
336
+ create(context) {
337
+ const [{ pattern = DEFAULT_TODO_PATTERN } = {}] = context.options;
338
+ const todoPattern = new RegExp(pattern, "iu");
339
+ const sourceCode = getSourceCode(context);
340
+
341
+ return {
342
+ Program() {
343
+ for (const comment of sourceCode.getAllComments()) {
344
+ const text = cleanCommentText(comment);
345
+ const todoSegments = text.match(/\b(?:TODO|FIXME|XXX)\b[^\n;]*/giu) ?? [];
346
+ if (todoSegments.some((segment) => !todoPattern.test(segment.trim()))) {
347
+ context.report({ loc: comment.loc, messageId: "invalidTodo" });
348
+ }
349
+ }
350
+ },
351
+ };
352
+ },
353
+ });
354
+
355
+ const noCommentedOutCode = createRule({
356
+ name: "/no-commented-out-code",
357
+ meta: {
358
+ type: "suggestion",
359
+ docs: {
360
+ description: "Flag comments that look like disabled TypeScript or JavaScript code.",
361
+ },
362
+ schema: [],
363
+ messages: {
364
+ commentedOutCode: "Remove commented-out code; version history should preserve old implementations.",
365
+ },
366
+ },
367
+ create(context) {
368
+ const sourceCode = getSourceCode(context);
369
+ return {
370
+ Program() {
371
+ for (const comment of sourceCode.getAllComments()) {
372
+ const text = cleanCommentText(comment);
373
+ if (/\b(?:TODO|FIXME|XXX)\b/iu.test(text)) {
374
+ continue;
375
+ }
376
+ if (isLikelyCodeComment(text)) {
377
+ context.report({ loc: comment.loc, messageId: "commentedOutCode" });
378
+ }
379
+ }
380
+ },
381
+ };
382
+ },
383
+ });
384
+
385
+ const noBooleanFlagArguments = createRule({
386
+ name: "/no-boolean-flag-arguments",
387
+ meta: {
388
+ type: "suggestion",
389
+ docs: {
390
+ description: "Discourage boolean selector arguments and boolean mode parameters.",
391
+ },
392
+ schema: [
393
+ {
394
+ type: "object",
395
+ additionalProperties: false,
396
+ properties: {
397
+ selectorParameterNames: {
398
+ type: "array",
399
+ items: { type: "string" },
400
+ },
401
+ },
402
+ },
403
+ ],
404
+ messages: {
405
+ booleanCallArgument:
406
+ "Boolean literal arguments hide intent at the call site; prefer a named operation or options object.",
407
+ booleanSelectorParameter:
408
+ "Boolean selector parameter '{{name}}' changes behavior by mode; prefer named operations or an explicit options type.",
409
+ },
410
+ },
411
+ create(context) {
412
+ const [{ selectorParameterNames = DEFAULT_SELECTOR_PARAM_NAMES } = {}] = context.options;
413
+
414
+ function checkParams(node) {
415
+ for (const parameter of node.params ?? []) {
416
+ const identifiers = collectPatternIdentifiers(parameter);
417
+ const annotatedNode = parameter.type === "AssignmentPattern" ? parameter.left : parameter;
418
+ for (const name of identifiers) {
419
+ if (isBooleanTypeAnnotation(annotatedNode) && nameLooksLikeSelector(name, selectorParameterNames)) {
420
+ context.report({
421
+ node: annotatedNode,
422
+ messageId: "booleanSelectorParameter",
423
+ data: { name },
424
+ });
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ function checkArguments(node) {
431
+ for (const argument of node.arguments ?? []) {
432
+ if (isBooleanLiteral(argument)) {
433
+ context.report({ node: argument, messageId: "booleanCallArgument" });
434
+ }
435
+ }
436
+ }
437
+
438
+ return {
439
+ ArrowFunctionExpression: checkParams,
440
+ FunctionDeclaration: checkParams,
441
+ FunctionExpression: checkParams,
442
+ CallExpression: checkArguments,
443
+ NewExpression: checkArguments,
444
+ };
445
+ },
446
+ });
447
+
448
+ const noOutputArgumentMutation = createRule({
449
+ name: "/no-output-argument-mutation",
450
+ meta: {
451
+ type: "suggestion",
452
+ docs: {
453
+ description: "Flag parameter mutation that treats arguments as output containers.",
454
+ },
455
+ schema: [
456
+ {
457
+ type: "object",
458
+ additionalProperties: false,
459
+ properties: {
460
+ mutatorMethods: {
461
+ type: "array",
462
+ items: { type: "string" },
463
+ },
464
+ },
465
+ },
466
+ ],
467
+ messages: {
468
+ outputArgument:
469
+ "Avoid mutating parameter '{{name}}' as an output argument; return a value or create a local copy instead.",
470
+ },
471
+ },
472
+ create(context) {
473
+ const [{ mutatorMethods = DEFAULT_MUTATOR_METHODS } = {}] = context.options;
474
+ const functionStack = [];
475
+
476
+ function enterFunction(node) {
477
+ functionStack.push({
478
+ locals: new Set(),
479
+ params: new Set((node.params ?? []).flatMap((parameter) => collectPatternIdentifiers(parameter))),
480
+ });
481
+ }
482
+
483
+ function exitFunction() {
484
+ functionStack.pop();
485
+ }
486
+
487
+ function reportIfParamMutation(node, expression) {
488
+ const name = getRootIdentifierName(expression);
489
+ if (!name) {
490
+ return;
491
+ }
492
+ for (let index = functionStack.length - 1; index >= 0; index -= 1) {
493
+ const scope = functionStack[index];
494
+ if (scope.params.has(name)) {
495
+ context.report({ node, messageId: "outputArgument", data: { name } });
496
+ return;
497
+ }
498
+ if (scope.locals.has(name)) {
499
+ return;
500
+ }
501
+ }
502
+ }
503
+
504
+ return {
505
+ ArrowFunctionExpression: enterFunction,
506
+ "ArrowFunctionExpression:exit": exitFunction,
507
+ FunctionDeclaration: enterFunction,
508
+ "FunctionDeclaration:exit": exitFunction,
509
+ FunctionExpression: enterFunction,
510
+ "FunctionExpression:exit": exitFunction,
511
+ VariableDeclarator(node) {
512
+ const scope = functionStack.at(-1);
513
+ if (!scope) {
514
+ return;
515
+ }
516
+ for (const name of collectPatternIdentifiers(node.id)) {
517
+ scope.locals.add(name);
518
+ }
519
+ },
520
+ AssignmentExpression(node) {
521
+ reportIfParamMutation(node.left, node.left);
522
+ },
523
+ UpdateExpression(node) {
524
+ reportIfParamMutation(node.argument, node.argument);
525
+ },
526
+ CallExpression(node) {
527
+ const callee = unwrapExpression(node.callee);
528
+ if (callee?.type !== "MemberExpression" && callee?.type !== "OptionalMemberExpression") {
529
+ return;
530
+ }
531
+ const methodName = getIdentifierName(callee.property);
532
+ if (methodName && mutatorMethods.includes(methodName)) {
533
+ reportIfParamMutation(callee.object, callee.object);
534
+ }
535
+ },
536
+ };
537
+ },
538
+ });
539
+
540
+ const noRedundantComment = createRule({
541
+ name: "/no-redundant-comment",
542
+ meta: {
543
+ type: "suggestion",
544
+ docs: {
545
+ description: "Flag comments that mostly repeat the following line of code.",
546
+ },
547
+ schema: [
548
+ {
549
+ type: "object",
550
+ additionalProperties: false,
551
+ properties: {
552
+ minSharedWords: { type: "integer", minimum: 1 },
553
+ minOverlapRatio: { type: "number", minimum: 0, maximum: 1 },
554
+ },
555
+ },
556
+ ],
557
+ messages: {
558
+ redundantComment: "Comment mostly repeats the next line; prefer making the code name carry the intent.",
559
+ },
560
+ },
561
+ create(context) {
562
+ const [{ minSharedWords = 2, minOverlapRatio = 0.65 } = {}] = context.options;
563
+ const sourceCode = getSourceCode(context);
564
+
565
+ return {
566
+ Program() {
567
+ for (const comment of sourceCode.getAllComments()) {
568
+ const commentWords = normalizeWords(cleanCommentText(comment));
569
+ if (commentWords.length < minSharedWords) {
570
+ continue;
571
+ }
572
+ const nextLineWords = normalizeWords(firstLineAfterComment(sourceCode, comment));
573
+ const nextLineWordSet = new Set(nextLineWords);
574
+ const sharedWords = commentWords.filter((word) => nextLineWordSet.has(word));
575
+ if (sharedWords.length >= minSharedWords && sharedWords.length / commentWords.length >= minOverlapRatio) {
576
+ context.report({ loc: comment.loc, messageId: "redundantComment" });
577
+ }
578
+ }
579
+ },
580
+ };
581
+ },
582
+ });
583
+
584
+ const noNoisyComments = createRule({
585
+ name: "/no-noisy-comments",
586
+ meta: {
587
+ type: "suggestion",
588
+ docs: {
589
+ description: "Flag separator, byline/date, and closing-brace comments.",
590
+ },
591
+ schema: [],
592
+ messages: {
593
+ separator: "Separator comments add visual noise; use file structure or named functions instead.",
594
+ byline: "Avoid author/date byline comments in source; version control already records authorship.",
595
+ closingBrace: "Avoid closing-brace comments; extract or shorten the block instead.",
596
+ },
597
+ },
598
+ create(context) {
599
+ const sourceCode = getSourceCode(context);
600
+ return {
601
+ Program() {
602
+ for (const comment of sourceCode.getAllComments()) {
603
+ const text = cleanCommentText(comment);
604
+ if (isSeparatorComment(text)) {
605
+ context.report({ loc: comment.loc, messageId: "separator" });
606
+ } else if (isBylineComment(text) || isDateOnlyComment(text)) {
607
+ context.report({ loc: comment.loc, messageId: "byline" });
608
+ } else if (isClosingBraceComment(sourceCode, comment)) {
609
+ context.report({ loc: comment.loc, messageId: "closingBrace" });
610
+ }
611
+ }
612
+ },
613
+ };
614
+ },
615
+ });
616
+
617
+ const noBusinessPolicyLiterals = createRule({
618
+ name: "/no-business-policy-literals",
619
+ meta: {
620
+ type: "suggestion",
621
+ docs: {
622
+ description: "Flag hard-coded policy literals in branch, return, and call expressions.",
623
+ },
624
+ schema: [
625
+ {
626
+ type: "object",
627
+ additionalProperties: false,
628
+ properties: {
629
+ allowedCalls: {
630
+ type: "array",
631
+ items: { type: "string" },
632
+ },
633
+ },
634
+ },
635
+ ],
636
+ messages: {
637
+ policyLiteral:
638
+ "Policy literal '{{value}}' should usually be a named constant or enum value so the rule is searchable.",
639
+ },
640
+ },
641
+ create(context) {
642
+ const [{ allowedCalls = [] } = {}] = context.options;
643
+ const callAllowlist = [...new Set([...DEFAULT_LITERAL_CALL_ALLOWLIST, ...allowedCalls])];
644
+
645
+ function reportPolicyLiteral(node, value) {
646
+ if (isTypeOnlyLiteral(node) || !literalLooksLikePolicy(value) || isAllowedLiteralContext(node, callAllowlist)) {
647
+ return;
648
+ }
649
+ context.report({
650
+ node,
651
+ messageId: "policyLiteral",
652
+ data: { value: String(value) },
653
+ });
654
+ }
655
+
656
+ return {
657
+ Literal(node) {
658
+ if (!isStringOrNumberLiteral(node)) {
659
+ return;
660
+ }
661
+ reportPolicyLiteral(node, node.value);
662
+ },
663
+ TemplateLiteral(node) {
664
+ if (node.expressions.length > 0 || node.quasis.length !== 1) {
665
+ return;
666
+ }
667
+ reportPolicyLiteral(node, node.quasis[0].value.cooked ?? node.quasis[0].value.raw);
668
+ },
669
+ };
670
+ },
671
+ });
672
+
673
+ const noTrainWrecks = createRule({
674
+ name: "/no-train-wrecks",
675
+ meta: {
676
+ type: "suggestion",
677
+ docs: {
678
+ description: "Flag deep property chains that expose transitive object structure.",
679
+ },
680
+ schema: [
681
+ {
682
+ type: "object",
683
+ additionalProperties: false,
684
+ properties: {
685
+ maxDepth: { type: "integer", minimum: 1 },
686
+ },
687
+ },
688
+ ],
689
+ messages: {
690
+ trainWreck: "Deep property chain exposes object internals; prefer a named query on the owning object.",
691
+ },
692
+ },
693
+ create(context) {
694
+ const [{ maxDepth = 3 } = {}] = context.options;
695
+
696
+ return {
697
+ MemberExpression(node) {
698
+ if (
699
+ isMemberDepthTooDeep(node, maxDepth) &&
700
+ node.parent?.type !== "MemberExpression" &&
701
+ node.parent?.type !== "OptionalMemberExpression"
702
+ ) {
703
+ context.report({ node, messageId: "trainWreck" });
704
+ }
705
+ },
706
+ OptionalMemberExpression(node) {
707
+ if (
708
+ isMemberDepthTooDeep(node, maxDepth) &&
709
+ node.parent?.type !== "MemberExpression" &&
710
+ node.parent?.type !== "OptionalMemberExpression"
711
+ ) {
712
+ context.report({ node, messageId: "trainWreck" });
713
+ }
714
+ },
715
+ };
716
+ },
717
+ });
718
+
719
+ const rules = {
720
+ "todo-format": todoFormat,
721
+ "no-commented-out-code": noCommentedOutCode,
722
+ "no-boolean-flag-arguments": noBooleanFlagArguments,
723
+ "no-output-argument-mutation": noOutputArgumentMutation,
724
+ "no-redundant-comment": noRedundantComment,
725
+ "no-noisy-comments": noNoisyComments,
726
+ "no-business-policy-literals": noBusinessPolicyLiterals,
727
+ "no-train-wrecks": noTrainWrecks,
728
+ };
729
+
730
+ const plugin = {
731
+ meta: {
732
+ name: "eslint-plugin-clean-code",
733
+ version: "0.1.0",
734
+ },
735
+ rules,
736
+ configs: {
737
+ recommended: {
738
+ plugins: {
739
+ "clean-code": undefined,
740
+ },
741
+ rules: {
742
+ "clean-code/todo-format": "warn",
743
+ "clean-code/no-commented-out-code": "warn",
744
+ "clean-code/no-boolean-flag-arguments": "warn",
745
+ "clean-code/no-output-argument-mutation": "warn",
746
+ "clean-code/no-redundant-comment": "warn",
747
+ "clean-code/no-noisy-comments": "warn",
748
+ "clean-code/no-business-policy-literals": "warn",
749
+ "clean-code/no-train-wrecks": "warn",
750
+ },
751
+ },
752
+ },
753
+ };
754
+
755
+ plugin.configs.recommended.plugins["clean-code"] = plugin;
756
+
757
+ export default plugin;
758
+ export { rules };