Skip to content

Commit 7bfcfd1

Browse files
Copilotalexr00
andcommitted
Implement public-methods-well-defined-types ESLint rule
Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 1797538 commit 7bfcfd1

File tree

5 files changed

+1571
-1347
lines changed

5 files changed

+1571
-1347
lines changed

.eslintrc.local.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
const RULES_DIR = require('eslint-plugin-rulesdir');
9+
RULES_DIR.RULES_DIR = './build/eslint-rules';
10+
11+
module.exports = {
12+
extends: ['.eslintrc.base.json'],
13+
env: {
14+
browser: true,
15+
node: true
16+
},
17+
parserOptions: {
18+
project: 'tsconfig.eslint.json'
19+
},
20+
plugins: ['rulesdir'],
21+
rules: {
22+
'rulesdir/public-methods-well-defined-types': 'error'
23+
}
24+
};

build/eslint-rules/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
module.exports = {
9+
'public-methods-well-defined-types': require('./public-methods-well-defined-types'),
10+
};
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
/**
9+
* ESLint rule to enforce that public methods in exported classes return well-defined types.
10+
* This rule ensures that no inline type (object literal, anonymous type, etc.) is returned
11+
* from any public method.
12+
*/
13+
14+
const { ESLintUtils } = require('@typescript-eslint/utils');
15+
16+
module.exports = {
17+
meta: {
18+
type: 'problem',
19+
docs: {
20+
description: 'Enforce that public methods return well-defined types (no inline types)',
21+
category: 'TypeScript',
22+
recommended: false,
23+
},
24+
schema: [],
25+
messages: {
26+
inlineReturnType: 'Public method "{{methodName}}" should return a well-defined type, not an inline type. Consider defining an interface or type alias.',
27+
},
28+
},
29+
30+
create(context) {
31+
/**
32+
* Check if a node represents an inline type that should be flagged
33+
*/
34+
function isInlineType(typeNode) {
35+
if (!typeNode) return false;
36+
37+
switch (typeNode.type) {
38+
// Object type literals: { foo: string, bar: number }
39+
case 'TSTypeLiteral':
40+
return true;
41+
42+
// Union types with inline object types: string | { foo: bar }
43+
case 'TSUnionType':
44+
return typeNode.types.some(isInlineType);
45+
46+
// Intersection types with inline object types: Base & { foo: bar }
47+
case 'TSIntersectionType':
48+
return typeNode.types.some(isInlineType);
49+
50+
// Tuple types: [string, number]
51+
case 'TSTupleType':
52+
return true;
53+
54+
// Mapped types: { [K in keyof T]: U }
55+
case 'TSMappedType':
56+
return true;
57+
58+
// Conditional types: T extends U ? X : Y (inline)
59+
case 'TSConditionalType':
60+
return true;
61+
62+
default:
63+
return false;
64+
}
65+
}
66+
67+
/**
68+
* Check if a method is public (not private or protected)
69+
*/
70+
function isPublicMethod(node) {
71+
// If no accessibility modifier is specified, it's public by default
72+
if (!node.accessibility) return true;
73+
return node.accessibility === 'public';
74+
}
75+
76+
/**
77+
* Check if a class is exported
78+
*/
79+
function isExportedClass(node) {
80+
// Check if the class declaration itself is exported
81+
if (node.parent && node.parent.type === 'ExportNamedDeclaration') {
82+
return true;
83+
}
84+
// Check if it's a default export
85+
if (node.parent && node.parent.type === 'ExportDefaultDeclaration') {
86+
return true;
87+
}
88+
return false;
89+
}
90+
91+
return {
92+
MethodDefinition(node) {
93+
// Only check methods in exported classes
94+
const classNode = node.parent.parent; // MethodDefinition -> ClassBody -> ClassDeclaration
95+
if (!classNode || classNode.type !== 'ClassDeclaration' || !isExportedClass(classNode)) {
96+
return;
97+
}
98+
99+
// Only check public methods
100+
if (!isPublicMethod(node)) {
101+
return;
102+
}
103+
104+
// Check if the method has a return type annotation
105+
const functionNode = node.value;
106+
if (!functionNode.returnType) {
107+
return; // No explicit return type, skip
108+
}
109+
110+
const returnTypeNode = functionNode.returnType.typeAnnotation;
111+
112+
// Check if the return type is an inline type
113+
if (isInlineType(returnTypeNode)) {
114+
const methodName = node.key.type === 'Identifier' ? node.key.name : '<computed>';
115+
context.report({
116+
node: functionNode.returnType,
117+
messageId: 'inlineReturnType',
118+
data: {
119+
methodName: methodName,
120+
},
121+
});
122+
}
123+
},
124+
125+
// Also check arrow function properties that are public methods
126+
PropertyDefinition(node) {
127+
// Only check properties in exported classes
128+
const classNode = node.parent.parent; // PropertyDefinition -> ClassBody -> ClassDeclaration
129+
if (!classNode || classNode.type !== 'ClassDeclaration' || !isExportedClass(classNode)) {
130+
return;
131+
}
132+
133+
// Only check public methods
134+
if (!isPublicMethod(node)) {
135+
return;
136+
}
137+
138+
// Check if the property is an arrow function
139+
if (node.value && node.value.type === 'ArrowFunctionExpression') {
140+
const arrowFunction = node.value;
141+
142+
// Check if the arrow function has a return type annotation
143+
if (!arrowFunction.returnType) {
144+
return; // No explicit return type, skip
145+
}
146+
147+
const returnTypeNode = arrowFunction.returnType.typeAnnotation;
148+
149+
// Check if the return type is an inline type
150+
if (isInlineType(returnTypeNode)) {
151+
const methodName = node.key.type === 'Identifier' ? node.key.name : '<computed>';
152+
context.report({
153+
node: arrowFunction.returnType,
154+
messageId: 'inlineReturnType',
155+
data: {
156+
methodName: methodName,
157+
},
158+
});
159+
}
160+
}
161+
}
162+
};
163+
},
164+
};

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,7 +2145,7 @@
21452145
"command": "pr.copyVscodeDevPrLink",
21462146
"when": "github:inReviewMode && remoteName != codespaces && embedderIdentifier != github.dev"
21472147
},
2148-
{
2148+
{
21492149
"command": "pr.copyPrLink",
21502150
"when": "false"
21512151
},
@@ -3332,7 +3332,6 @@
33323332
"command": "pr.openSessionLogFromDescription",
33333333
"when": "webviewId == PullRequestOverview && github:codingAgentMenu"
33343334
}
3335-
33363335
],
33373336
"chat/chatSessions": [
33383337
{
@@ -4054,6 +4053,7 @@
40544053
"eslint": "7.22.0",
40554054
"eslint-cli": "1.1.1",
40564055
"eslint-plugin-import": "2.22.1",
4056+
"eslint-plugin-rulesdir": "^0.2.2",
40574057
"event-stream": "^4.0.1",
40584058
"fork-ts-checker-webpack-plugin": "6.1.1",
40594059
"glob": "7.1.6",

0 commit comments

Comments
 (0)