eslint-plugin-modularity 2.0.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/README.md +161 -0
- package/package.json +54 -0
- package/src/index.d.ts +16 -0
- package/src/index.js +59 -0
- package/src/index.js.map +1 -0
- package/src/lib/eslint-plugin-modularity.d.ts +1 -0
- package/src/lib/eslint-plugin-modularity.js +7 -0
- package/src/lib/eslint-plugin-modularity.js.map +1 -0
- package/src/rules/ddd-anemic-domain-model.d.ts +27 -0
- package/src/rules/ddd-anemic-domain-model.js +285 -0
- package/src/rules/ddd-anemic-domain-model.js.map +1 -0
- package/src/rules/ddd-value-object-immutability.d.ts +27 -0
- package/src/rules/ddd-value-object-immutability.js +197 -0
- package/src/rules/ddd-value-object-immutability.js.map +1 -0
- package/src/rules/enforce-naming.d.ts +30 -0
- package/src/rules/enforce-naming.js +212 -0
- package/src/rules/enforce-naming.js.map +1 -0
- package/src/rules/enforce-rest-conventions.d.ts +27 -0
- package/src/rules/enforce-rest-conventions.js +162 -0
- package/src/rules/enforce-rest-conventions.js.map +1 -0
- package/src/rules/no-external-api-calls-in-utils.d.ts +23 -0
- package/src/rules/no-external-api-calls-in-utils.js +151 -0
- package/src/rules/no-external-api-calls-in-utils.js.map +1 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.dddValueObjectImmutability = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Check if a class name suggests a value object
|
|
13
|
+
*/
|
|
14
|
+
function isValueObject(className, patterns) {
|
|
15
|
+
return patterns.some(pattern => className.includes(pattern));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if property has readonly modifier
|
|
19
|
+
*/
|
|
20
|
+
function hasReadonlyModifier(node) {
|
|
21
|
+
if (node.type === 'PropertyDefinition') {
|
|
22
|
+
return node.readonly === true;
|
|
23
|
+
}
|
|
24
|
+
/* v8 ignore start -- TSPropertySignature is for interfaces, rule only processes classes */
|
|
25
|
+
if (node.type === 'TSPropertySignature') {
|
|
26
|
+
return node.readonly === true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
/* v8 ignore stop */
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if class uses Object.freeze
|
|
33
|
+
*/
|
|
34
|
+
function usesObjectFreeze(node) {
|
|
35
|
+
// Check constructor for Object.freeze(this)
|
|
36
|
+
for (const member of node.body.body) {
|
|
37
|
+
if (member.type === 'MethodDefinition' && member.kind === 'constructor') {
|
|
38
|
+
const constructor = member.value;
|
|
39
|
+
if (constructor.type === 'FunctionExpression') {
|
|
40
|
+
const body = constructor.body;
|
|
41
|
+
if (body.type === 'BlockStatement') {
|
|
42
|
+
for (const statement of body.body) {
|
|
43
|
+
if (statement.type === 'ExpressionStatement' &&
|
|
44
|
+
statement.expression.type === 'CallExpression' &&
|
|
45
|
+
statement.expression.callee.type === 'MemberExpression' &&
|
|
46
|
+
statement.expression.callee.object.type === 'Identifier' &&
|
|
47
|
+
statement.expression.callee.object.name === 'Object' &&
|
|
48
|
+
statement.expression.callee.property.type === 'Identifier' &&
|
|
49
|
+
statement.expression.callee.property.name === 'freeze') {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
exports.dddValueObjectImmutability = (0, eslint_devkit_2.createRule)({
|
|
60
|
+
name: 'ddd-value-object-immutability',
|
|
61
|
+
meta: {
|
|
62
|
+
type: 'suggestion',
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Validates value objects are properly immutable',
|
|
65
|
+
},
|
|
66
|
+
messages: {
|
|
67
|
+
mutableValueObject: (0, eslint_devkit_1.formatLLMMessage)({
|
|
68
|
+
icon: eslint_devkit_1.MessageIcons.ARCHITECTURE,
|
|
69
|
+
issueName: 'Mutable value object',
|
|
70
|
+
description: 'Value object {{className}} has mutable properties',
|
|
71
|
+
severity: 'MEDIUM',
|
|
72
|
+
fix: 'Make properties readonly or use Object.freeze',
|
|
73
|
+
documentationLink: 'https://martinfowler.com/bliki/ValueObject.html',
|
|
74
|
+
}),
|
|
75
|
+
mutableNestedType: (0, eslint_devkit_1.formatLLMMessage)({
|
|
76
|
+
icon: eslint_devkit_1.MessageIcons.ARCHITECTURE,
|
|
77
|
+
issueName: 'Mutable nested type',
|
|
78
|
+
description: 'Value object {{className}} has mutable nested type (array/object) - use Readonly<T> or readonly arrays',
|
|
79
|
+
severity: 'MEDIUM',
|
|
80
|
+
fix: 'Use readonly arrays or Readonly<T> for nested objects',
|
|
81
|
+
documentationLink: 'https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype',
|
|
82
|
+
}),
|
|
83
|
+
addReadonly: (0, eslint_devkit_1.formatLLMMessage)({
|
|
84
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
85
|
+
issueName: 'Add Readonly',
|
|
86
|
+
description: 'Add readonly modifier',
|
|
87
|
+
severity: 'LOW',
|
|
88
|
+
fix: 'readonly propertyName: Type',
|
|
89
|
+
documentationLink: 'https://www.typescriptlang.org/docs/handbook/2/classes.html#readonly',
|
|
90
|
+
}),
|
|
91
|
+
useObjectFreeze: (0, eslint_devkit_1.formatLLMMessage)({
|
|
92
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
93
|
+
issueName: 'Use Object.freeze',
|
|
94
|
+
description: 'Use Object.freeze in constructor',
|
|
95
|
+
severity: 'LOW',
|
|
96
|
+
fix: 'constructor() { Object.freeze(this); }',
|
|
97
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze',
|
|
98
|
+
}),
|
|
99
|
+
makeImmutable: (0, eslint_devkit_1.formatLLMMessage)({
|
|
100
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
101
|
+
issueName: 'Make Immutable',
|
|
102
|
+
description: 'Make value object fully immutable',
|
|
103
|
+
severity: 'LOW',
|
|
104
|
+
fix: 'Add readonly to all properties and freeze object',
|
|
105
|
+
documentationLink: 'https://martinfowler.com/bliki/ValueObject.html',
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
schema: [
|
|
109
|
+
{
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
valueObjectPatterns: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
items: { type: 'string' },
|
|
115
|
+
default: ['Value', 'VO', 'ValueObject'],
|
|
116
|
+
description: 'Patterns to identify value objects',
|
|
117
|
+
},
|
|
118
|
+
requireReadonly: {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
default: true,
|
|
121
|
+
description: 'Require readonly modifiers',
|
|
122
|
+
},
|
|
123
|
+
checkObjectFreeze: {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
default: true,
|
|
126
|
+
description: 'Check for Object.freeze usage',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
defaultOptions: [
|
|
134
|
+
{
|
|
135
|
+
valueObjectPatterns: ['Value', 'VO', 'ValueObject'],
|
|
136
|
+
requireReadonly: true,
|
|
137
|
+
checkObjectFreeze: true,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
create(context, [options = {}]) {
|
|
141
|
+
const { valueObjectPatterns = ['Value', 'VO', 'ValueObject'], requireReadonly = true, checkObjectFreeze = true, } = options || {};
|
|
142
|
+
/**
|
|
143
|
+
* Check class declarations
|
|
144
|
+
*/
|
|
145
|
+
function checkClass(node) {
|
|
146
|
+
if (!node.id) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const className = node.id.name;
|
|
150
|
+
// Check if it's a value object
|
|
151
|
+
if (!isValueObject(className, valueObjectPatterns)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Check if Object.freeze is used
|
|
155
|
+
if (checkObjectFreeze && usesObjectFreeze(node)) {
|
|
156
|
+
return; // Already using Object.freeze
|
|
157
|
+
}
|
|
158
|
+
// Check properties for readonly
|
|
159
|
+
const mutableProperties = [];
|
|
160
|
+
for (const member of node.body.body) {
|
|
161
|
+
if (member.type === 'PropertyDefinition') {
|
|
162
|
+
if (requireReadonly && !hasReadonlyModifier(member)) {
|
|
163
|
+
mutableProperties.push(member);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (mutableProperties.length > 0) {
|
|
168
|
+
context.report({
|
|
169
|
+
node,
|
|
170
|
+
messageId: 'mutableValueObject',
|
|
171
|
+
data: {
|
|
172
|
+
className,
|
|
173
|
+
},
|
|
174
|
+
suggest: [
|
|
175
|
+
{
|
|
176
|
+
messageId: 'addReadonly',
|
|
177
|
+
fix: () => null, // Complex fix, requires AST manipulation
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
messageId: 'useObjectFreeze',
|
|
181
|
+
fix: () => null,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
messageId: 'makeImmutable',
|
|
185
|
+
fix: () => null,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
ClassDeclaration: checkClass,
|
|
193
|
+
ClassExpression: checkClass,
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=ddd-value-object-immutability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddd-value-object-immutability.js","sourceRoot":"","sources":["../../../../../packages/eslint-plugin-modularity/src/rules/ddd-value-object-immutability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: enforce-naming
|
|
8
|
+
* Enforce domain-specific naming conventions with business context
|
|
9
|
+
*/
|
|
10
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
11
|
+
type MessageIds = 'wrongTerminology' | 'useDomainTerm' | 'viewGlossary';
|
|
12
|
+
interface DomainTerm {
|
|
13
|
+
incorrect: string | RegExp;
|
|
14
|
+
correct: string;
|
|
15
|
+
context: string;
|
|
16
|
+
examples?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface Options {
|
|
19
|
+
/** Domain context for naming conventions (e.g., 'ecommerce', 'healthcare') */
|
|
20
|
+
domain?: string;
|
|
21
|
+
/** Array of domain-specific terminology rules */
|
|
22
|
+
terms?: DomainTerm[];
|
|
23
|
+
/** URL to domain glossary documentation */
|
|
24
|
+
glossaryUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
type RuleOptions = [Options?];
|
|
27
|
+
export declare const enforceNaming: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.enforceNaming = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.enforceNaming = (0, eslint_devkit_2.createRule)({
|
|
12
|
+
name: 'enforce-naming',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'suggestion',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce domain-specific naming conventions with business context',
|
|
17
|
+
},
|
|
18
|
+
fixable: 'code',
|
|
19
|
+
hasSuggestions: true,
|
|
20
|
+
messages: {
|
|
21
|
+
// 🎯 Token optimization: 46% reduction (56→30 tokens) - domain terminology keeps code clear
|
|
22
|
+
wrongTerminology: (0, eslint_devkit_1.formatLLMMessage)({
|
|
23
|
+
icon: eslint_devkit_1.MessageIcons.QUALITY,
|
|
24
|
+
issueName: 'Domain terminology mismatch',
|
|
25
|
+
cwe: 'CWE-216',
|
|
26
|
+
description: 'Domain terminology mismatch',
|
|
27
|
+
severity: 'MEDIUM',
|
|
28
|
+
fix: 'Use "{{correctTerm}}" ({{context}}) for ubiquitous language alignment',
|
|
29
|
+
documentationLink: 'Domain glossary',
|
|
30
|
+
}),
|
|
31
|
+
useDomainTerm: (0, eslint_devkit_1.formatLLMMessage)({
|
|
32
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
33
|
+
issueName: 'Use Domain Term',
|
|
34
|
+
description: 'Replace with domain terminology',
|
|
35
|
+
severity: 'LOW',
|
|
36
|
+
fix: 'Use "{{correctTerm}}" instead',
|
|
37
|
+
documentationLink: 'https://martinfowler.com/bliki/UbiquitousLanguage.html',
|
|
38
|
+
}),
|
|
39
|
+
viewGlossary: (0, eslint_devkit_1.formatLLMMessage)({
|
|
40
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
41
|
+
issueName: 'View Glossary',
|
|
42
|
+
description: 'View domain glossary',
|
|
43
|
+
severity: 'LOW',
|
|
44
|
+
fix: 'Reference domain glossary for correct terminology',
|
|
45
|
+
documentationLink: 'https://martinfowler.com/bliki/UbiquitousLanguage.html',
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
schema: [
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
domain: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
default: 'general',
|
|
55
|
+
},
|
|
56
|
+
terms: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
incorrect: { type: ['string', 'object'] }, // string or regex
|
|
62
|
+
correct: { type: 'string' },
|
|
63
|
+
context: { type: 'string' },
|
|
64
|
+
examples: {
|
|
65
|
+
type: 'array',
|
|
66
|
+
items: { type: 'string' },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['incorrect', 'correct', 'context'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
glossaryUrl: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
defaultOptions: [
|
|
81
|
+
{
|
|
82
|
+
domain: 'general',
|
|
83
|
+
terms: [],
|
|
84
|
+
glossaryUrl: undefined,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
create(context) {
|
|
88
|
+
const options = context.options[0] || {};
|
|
89
|
+
const { domain = 'general', terms = [] } = options || {};
|
|
90
|
+
// Early return if no terms configured
|
|
91
|
+
if (!terms || terms.length === 0) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if identifier violates domain terms
|
|
96
|
+
*/
|
|
97
|
+
const checkIdentifier = (name) => {
|
|
98
|
+
for (const term of terms) {
|
|
99
|
+
if (typeof term.incorrect === 'string') {
|
|
100
|
+
if (name.toLowerCase().includes(term.incorrect.toLowerCase())) {
|
|
101
|
+
return term;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (term.incorrect instanceof RegExp) {
|
|
105
|
+
if (term.incorrect.test(name)) {
|
|
106
|
+
return term;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Preserve case when replacing term
|
|
114
|
+
* e.g., User -> Customer, user -> customer, USER -> CUSTOMER
|
|
115
|
+
*/
|
|
116
|
+
const preserveCase = (original, replacement) => {
|
|
117
|
+
if (original === original.toUpperCase()) {
|
|
118
|
+
return replacement.toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
if (original[0] === original[0].toUpperCase()) {
|
|
121
|
+
return replacement.charAt(0).toUpperCase() + replacement.slice(1);
|
|
122
|
+
}
|
|
123
|
+
return replacement.toLowerCase();
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Generate replacement suggestion
|
|
127
|
+
*/
|
|
128
|
+
const generateReplacement = (name, term) => {
|
|
129
|
+
if (typeof term.incorrect === 'string') {
|
|
130
|
+
// Case-preserving replacement
|
|
131
|
+
return name.replace(new RegExp(`(${term.incorrect})`, 'gi'), (match) => {
|
|
132
|
+
return preserveCase(match, term.correct);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// For regex, just suggest the correct term
|
|
136
|
+
return term.correct;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Report violation
|
|
140
|
+
*/
|
|
141
|
+
const reportViolation = (node, violatedTerm) => {
|
|
142
|
+
const replacement = generateReplacement(node.name, violatedTerm);
|
|
143
|
+
context.report({
|
|
144
|
+
node,
|
|
145
|
+
messageId: 'wrongTerminology',
|
|
146
|
+
data: {
|
|
147
|
+
incorrectTerm: node.name,
|
|
148
|
+
correctTerm: violatedTerm.correct,
|
|
149
|
+
context: violatedTerm.context || domain,
|
|
150
|
+
domain,
|
|
151
|
+
},
|
|
152
|
+
suggest: [
|
|
153
|
+
{
|
|
154
|
+
messageId: 'useDomainTerm',
|
|
155
|
+
data: { correctTerm: violatedTerm.correct },
|
|
156
|
+
fix: (fixer) => {
|
|
157
|
+
return fixer.replaceText(node, replacement);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
// Check variable declarations
|
|
165
|
+
VariableDeclarator(node) {
|
|
166
|
+
if (node.id.type !== 'Identifier')
|
|
167
|
+
return;
|
|
168
|
+
const violatedTerm = checkIdentifier(node.id.name);
|
|
169
|
+
if (violatedTerm) {
|
|
170
|
+
reportViolation(node.id, violatedTerm);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
// Check function declarations
|
|
174
|
+
FunctionDeclaration(node) {
|
|
175
|
+
if (!node.id)
|
|
176
|
+
return;
|
|
177
|
+
const violatedTerm = checkIdentifier(node.id.name);
|
|
178
|
+
if (violatedTerm) {
|
|
179
|
+
reportViolation(node.id, violatedTerm);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
// Check class declarations
|
|
183
|
+
ClassDeclaration(node) {
|
|
184
|
+
if (!node.id)
|
|
185
|
+
return;
|
|
186
|
+
const violatedTerm = checkIdentifier(node.id.name);
|
|
187
|
+
if (violatedTerm) {
|
|
188
|
+
reportViolation(node.id, violatedTerm);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
// Check property definitions
|
|
192
|
+
PropertyDefinition(node) {
|
|
193
|
+
if (node.key.type !== 'Identifier')
|
|
194
|
+
return;
|
|
195
|
+
const violatedTerm = checkIdentifier(node.key.name);
|
|
196
|
+
if (violatedTerm) {
|
|
197
|
+
reportViolation(node.key, violatedTerm);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
// Check method definitions
|
|
201
|
+
MethodDefinition(node) {
|
|
202
|
+
if (node.key.type !== 'Identifier')
|
|
203
|
+
return;
|
|
204
|
+
const violatedTerm = checkIdentifier(node.key.name);
|
|
205
|
+
if (violatedTerm) {
|
|
206
|
+
reportViolation(node.key, violatedTerm);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=enforce-naming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce-naming.js","sourceRoot":"","sources":["../../../../../packages/eslint-plugin-modularity/src/rules/enforce-naming.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: enforce-rest-conventions
|
|
8
|
+
* Validates REST endpoint design against best practices
|
|
9
|
+
* Priority 6: API Design & Evolution
|
|
10
|
+
*
|
|
11
|
+
* @see https://restfulapi.net/
|
|
12
|
+
*/
|
|
13
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
14
|
+
type MessageIds = 'restConventionViolation' | 'useProperHttpMethod' | 'useProperStatusCode' | 'fixResourceNaming';
|
|
15
|
+
export interface Options {
|
|
16
|
+
/** Check HTTP methods. Default: true */
|
|
17
|
+
checkHttpMethods?: boolean;
|
|
18
|
+
/** Check status codes. Default: true */
|
|
19
|
+
checkStatusCodes?: boolean;
|
|
20
|
+
/** Check resource naming. Default: true */
|
|
21
|
+
checkResourceNaming?: boolean;
|
|
22
|
+
}
|
|
23
|
+
type RuleOptions = [Options?];
|
|
24
|
+
export declare const enforceRestConventions: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.enforceRestConventions = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Valid HTTP methods for REST
|
|
13
|
+
*/
|
|
14
|
+
const VALID_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
15
|
+
/**
|
|
16
|
+
* Check if HTTP method is valid
|
|
17
|
+
*/
|
|
18
|
+
function isValidHttpMethod(method) {
|
|
19
|
+
return VALID_HTTP_METHODS.includes(method.toUpperCase());
|
|
20
|
+
}
|
|
21
|
+
exports.enforceRestConventions = (0, eslint_devkit_2.createRule)({
|
|
22
|
+
name: 'enforce-rest-conventions',
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'suggestion',
|
|
25
|
+
docs: {
|
|
26
|
+
description: 'Validates REST endpoint design against best practices',
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
restConventionViolation: (0, eslint_devkit_1.formatLLMMessage)({
|
|
30
|
+
icon: eslint_devkit_1.MessageIcons.ARCHITECTURE,
|
|
31
|
+
issueName: 'REST convention violation',
|
|
32
|
+
description: '{{violation}}: {{details}}',
|
|
33
|
+
severity: 'MEDIUM',
|
|
34
|
+
fix: 'Follow RESTful conventions for HTTP methods, status codes, and resource naming',
|
|
35
|
+
documentationLink: 'https://restfulapi.net/',
|
|
36
|
+
}),
|
|
37
|
+
useProperHttpMethod: (0, eslint_devkit_1.formatLLMMessage)({
|
|
38
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
39
|
+
issueName: 'Use HTTP Method',
|
|
40
|
+
description: 'Use proper HTTP method',
|
|
41
|
+
severity: 'LOW',
|
|
42
|
+
fix: 'GET (read), POST (create), PUT/PATCH (update), DELETE (delete)',
|
|
43
|
+
documentationLink: 'https://restfulapi.net/http-methods/',
|
|
44
|
+
}),
|
|
45
|
+
useProperStatusCode: (0, eslint_devkit_1.formatLLMMessage)({
|
|
46
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
47
|
+
issueName: 'Use Status Code',
|
|
48
|
+
description: 'Use appropriate status code',
|
|
49
|
+
severity: 'LOW',
|
|
50
|
+
fix: '200 (OK), 201 (Created), 204 (No Content), 400 (Bad Request), 404 (Not Found)',
|
|
51
|
+
documentationLink: 'https://restfulapi.net/http-status-codes/',
|
|
52
|
+
}),
|
|
53
|
+
fixResourceNaming: (0, eslint_devkit_1.formatLLMMessage)({
|
|
54
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
55
|
+
issueName: 'Resource Naming',
|
|
56
|
+
description: 'Use plural nouns for resources',
|
|
57
|
+
severity: 'LOW',
|
|
58
|
+
fix: '/users, /orders, /products (not /user, /order)',
|
|
59
|
+
documentationLink: 'https://restfulapi.net/resource-naming/',
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
schema: [
|
|
63
|
+
{
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
checkHttpMethods: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
default: true,
|
|
69
|
+
description: 'Check HTTP methods',
|
|
70
|
+
},
|
|
71
|
+
checkStatusCodes: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
default: true,
|
|
74
|
+
description: 'Check status codes',
|
|
75
|
+
},
|
|
76
|
+
checkResourceNaming: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
default: true,
|
|
79
|
+
description: 'Check resource naming',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
defaultOptions: [
|
|
87
|
+
{
|
|
88
|
+
checkHttpMethods: true,
|
|
89
|
+
checkStatusCodes: true,
|
|
90
|
+
checkResourceNaming: true,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
create(context, [options = {}]) {
|
|
94
|
+
const { checkHttpMethods = true, checkStatusCodes = true, checkResourceNaming = true, } = options || {};
|
|
95
|
+
/**
|
|
96
|
+
* Check CallExpression for REST API patterns
|
|
97
|
+
*/
|
|
98
|
+
function checkCallExpression(node) {
|
|
99
|
+
// Check for Express/Fastify route handlers: app.get(), router.post(), etc.
|
|
100
|
+
if (node.callee.type === 'MemberExpression') {
|
|
101
|
+
const property = node.callee.property;
|
|
102
|
+
if (property.type === 'Identifier') {
|
|
103
|
+
const methodName = property.name.toLowerCase();
|
|
104
|
+
const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
105
|
+
if (httpMethods.includes(methodName)) {
|
|
106
|
+
// Check HTTP method
|
|
107
|
+
if (checkHttpMethods && !isValidHttpMethod(methodName)) {
|
|
108
|
+
context.report({
|
|
109
|
+
node: property,
|
|
110
|
+
messageId: 'restConventionViolation',
|
|
111
|
+
data: {
|
|
112
|
+
violation: 'Invalid HTTP method',
|
|
113
|
+
details: `"${methodName}" is not a valid REST HTTP method`,
|
|
114
|
+
},
|
|
115
|
+
suggest: [
|
|
116
|
+
{ messageId: 'useProperHttpMethod', fix: () => null },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Check resource naming (first argument should be a path)
|
|
121
|
+
if (checkResourceNaming && node.arguments.length > 0) {
|
|
122
|
+
const pathArg = node.arguments[0];
|
|
123
|
+
if (pathArg.type === 'Literal' && typeof pathArg.value === 'string') {
|
|
124
|
+
const path = pathArg.value;
|
|
125
|
+
// Check if path uses plural nouns (basic check)
|
|
126
|
+
if (path.startsWith('/') && path.length > 1) {
|
|
127
|
+
const resource = path.split('/')[1];
|
|
128
|
+
// Basic check: resource should be plural or have :id
|
|
129
|
+
if (!path.includes(':') && !resource.endsWith('s') && resource.length > 3) {
|
|
130
|
+
context.report({
|
|
131
|
+
node: pathArg,
|
|
132
|
+
messageId: 'restConventionViolation',
|
|
133
|
+
data: {
|
|
134
|
+
violation: 'Resource naming',
|
|
135
|
+
details: `Resource "${resource}" should be plural (e.g., /users, /orders)`,
|
|
136
|
+
},
|
|
137
|
+
suggest: [
|
|
138
|
+
{ messageId: 'fixResourceNaming', fix: () => null },
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Check for status code usage: res.status(200)
|
|
149
|
+
if (checkStatusCodes && node.callee.type === 'MemberExpression') {
|
|
150
|
+
const property = node.callee.property;
|
|
151
|
+
if (property.type === 'Identifier' && property.name === 'status') {
|
|
152
|
+
// Check if status code is appropriate (would need to track HTTP method)
|
|
153
|
+
// This is simplified - full implementation would track the HTTP method from parent
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
CallExpression: checkCallExpression,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
//# sourceMappingURL=enforce-rest-conventions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce-rest-conventions.js","sourceRoot":"","sources":["../../../../../packages/eslint-plugin-modularity/src/rules/enforce-rest-conventions.ts"],"names":[],"mappings":"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"}
|