UNPKG

10.3 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5}) : (function(o, m, k, k2) {
6 if (k2 === undefined) k2 = k;
7 o[k2] = m[k];
8}));
9var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 Object.defineProperty(o, "default", { enumerable: true, value: v });
11}) : function(o, v) {
12 o["default"] = v;
13});
14var __importStar = (this && this.__importStar) || function (mod) {
15 if (mod && mod.__esModule) return mod;
16 var result = {};
17 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 __setModuleDefault(result, mod);
19 return result;
20};
21Object.defineProperty(exports, "__esModule", { value: true });
22exports.phrases = void 0;
23const utils_1 = require("@typescript-eslint/utils");
24const util = __importStar(require("../util"));
25exports.phrases = {
26 [utils_1.AST_NODE_TYPES.TSTypeLiteral]: 'Type literal',
27 [utils_1.AST_NODE_TYPES.TSInterfaceDeclaration]: 'Interface',
28};
29exports.default = util.createRule({
30 name: 'prefer-function-type',
31 meta: {
32 docs: {
33 description: 'Use function types instead of interfaces with call signatures',
34 recommended: false,
35 },
36 fixable: 'code',
37 messages: {
38 functionTypeOverCallableType: '{{ literalOrInterface }} only has a call signature, you should use a function type instead.',
39 unexpectedThisOnFunctionOnlyInterface: "`this` refers to the function type '{{ interfaceName }}', did you intend to use a generic `this` parameter like `<Self>(this: Self, ...) => Self` instead?",
40 },
41 schema: [],
42 type: 'suggestion',
43 },
44 defaultOptions: [],
45 create(context) {
46 const sourceCode = context.getSourceCode();
47 /**
48 * Checks if there the interface has exactly one supertype that isn't named 'Function'
49 * @param node The node being checked
50 */
51 function hasOneSupertype(node) {
52 if (!node.extends || node.extends.length === 0) {
53 return false;
54 }
55 if (node.extends.length !== 1) {
56 return true;
57 }
58 const expr = node.extends[0].expression;
59 return (expr.type !== utils_1.AST_NODE_TYPES.Identifier || expr.name !== 'Function');
60 }
61 /**
62 * @param parent The parent of the call signature causing the diagnostic
63 */
64 function shouldWrapSuggestion(parent) {
65 if (!parent) {
66 return false;
67 }
68 switch (parent.type) {
69 case utils_1.AST_NODE_TYPES.TSUnionType:
70 case utils_1.AST_NODE_TYPES.TSIntersectionType:
71 case utils_1.AST_NODE_TYPES.TSArrayType:
72 return true;
73 default:
74 return false;
75 }
76 }
77 /**
78 * @param member The TypeElement being checked
79 * @param node The parent of member being checked
80 * @param tsThisTypes
81 */
82 function checkMember(member, node, tsThisTypes = null) {
83 if ((member.type === utils_1.AST_NODE_TYPES.TSCallSignatureDeclaration ||
84 member.type === utils_1.AST_NODE_TYPES.TSConstructSignatureDeclaration) &&
85 typeof member.returnType !== 'undefined') {
86 if (tsThisTypes !== null &&
87 tsThisTypes.length > 0 &&
88 node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
89 // the message can be confusing if we don't point directly to the `this` node instead of the whole member
90 // and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple
91 context.report({
92 node: tsThisTypes[0],
93 messageId: 'unexpectedThisOnFunctionOnlyInterface',
94 data: {
95 interfaceName: node.id.name,
96 },
97 });
98 return;
99 }
100 const fixable = node.parent &&
101 node.parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration;
102 context.report({
103 node: member,
104 messageId: 'functionTypeOverCallableType',
105 data: {
106 literalOrInterface: exports.phrases[node.type],
107 },
108 fix: fixable
109 ? null
110 : (fixer) => {
111 const fixes = [];
112 const start = member.range[0];
113 const colonPos = member.returnType.range[0] - start;
114 const text = sourceCode.getText().slice(start, member.range[1]);
115 const comments = sourceCode
116 .getCommentsBefore(member)
117 .concat(sourceCode.getCommentsAfter(member));
118 let suggestion = `${text.slice(0, colonPos)} =>${text.slice(colonPos + 1)}`;
119 const lastChar = suggestion.endsWith(';') ? ';' : '';
120 if (lastChar) {
121 suggestion = suggestion.slice(0, -1);
122 }
123 if (shouldWrapSuggestion(node.parent)) {
124 suggestion = `(${suggestion})`;
125 }
126 if (node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
127 if (typeof node.typeParameters !== 'undefined') {
128 suggestion = `type ${sourceCode
129 .getText()
130 .slice(node.id.range[0], node.typeParameters.range[1])} = ${suggestion}${lastChar}`;
131 }
132 else {
133 suggestion = `type ${node.id.name} = ${suggestion}${lastChar}`;
134 }
135 }
136 const isParentExported = node.parent &&
137 node.parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
138 if (node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
139 isParentExported) {
140 const commentsText = comments.reduce((text, comment) => {
141 return (text +
142 (comment.type === utils_1.AST_TOKEN_TYPES.Line
143 ? `//${comment.value}`
144 : `/*${comment.value}*/`) +
145 '\n');
146 }, '');
147 // comments should move before export and not between export and interface declaration
148 fixes.push(fixer.insertTextBefore(node.parent, commentsText));
149 }
150 else {
151 comments.forEach(comment => {
152 let commentText = comment.type === utils_1.AST_TOKEN_TYPES.Line
153 ? `//${comment.value}`
154 : `/*${comment.value}*/`;
155 const isCommentOnTheSameLine = comment.loc.start.line === member.loc.start.line;
156 if (!isCommentOnTheSameLine) {
157 commentText += '\n';
158 }
159 else {
160 commentText += ' ';
161 }
162 suggestion = commentText + suggestion;
163 });
164 }
165 const fixStart = node.range[0];
166 fixes.push(fixer.replaceTextRange([fixStart, node.range[1]], suggestion));
167 return fixes;
168 },
169 });
170 }
171 }
172 let tsThisTypes = null;
173 let literalNesting = 0;
174 return {
175 TSInterfaceDeclaration() {
176 // when entering an interface reset the count of `this`s to empty.
177 tsThisTypes = [];
178 },
179 'TSInterfaceDeclaration TSThisType'(node) {
180 // inside an interface keep track of all ThisType references.
181 // unless it's inside a nested type literal in which case it's invalid code anyway
182 // we don't want to incorrectly say "it refers to name" while typescript says it's completely invalid.
183 if (literalNesting === 0 && tsThisTypes !== null) {
184 tsThisTypes.push(node);
185 }
186 },
187 'TSInterfaceDeclaration:exit'(node) {
188 if (!hasOneSupertype(node) && node.body.body.length === 1) {
189 checkMember(node.body.body[0], node, tsThisTypes);
190 }
191 // on exit check member and reset the array to nothing.
192 tsThisTypes = null;
193 },
194 // keep track of nested literals to avoid complaining about invalid `this` uses
195 'TSInterfaceDeclaration TSTypeLiteral'() {
196 literalNesting += 1;
197 },
198 'TSInterfaceDeclaration TSTypeLiteral:exit'() {
199 literalNesting -= 1;
200 },
201 'TSTypeLiteral[members.length = 1]'(node) {
202 checkMember(node.members[0], node);
203 },
204 };
205 },
206});
207//# sourceMappingURL=prefer-function-type.js.map
\No newline at end of file