/
id-length.js
210 lines (184 loc) · 7.46 KB
/
id-length.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* @fileoverview Rule that warns when identifier names are shorter or longer
* than the values provided in configuration.
* @author Burak Yigit Kaya aka BYK
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const GraphemeSplitter = require("grapheme-splitter");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks if the string given as argument is ASCII or not.
* @param {string} value A string that you want to know if it is ASCII or not.
* @returns {boolean} `true` if `value` is ASCII string.
*/
function isASCII(value) {
if (typeof value !== "string") {
return false;
}
return /^[\u0020-\u007f]*$/u.test(value);
}
/** @type {GraphemeSplitter | undefined} */
let splitter;
/**
* Gets the length of the string. If the string is not in ASCII, counts graphemes.
* @param {string} value A string that you want to get the length.
* @returns {number} The length of `value`.
*/
function getStringLength(value) {
if (isASCII(value)) {
return value.length;
}
if (!splitter) {
splitter = new GraphemeSplitter();
}
return splitter.countGraphemes(value);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Enforce minimum and maximum identifier lengths",
recommended: false,
url: "https://eslint.org/docs/rules/id-length"
},
schema: [
{
type: "object",
properties: {
min: {
type: "integer",
default: 2
},
max: {
type: "integer"
},
exceptions: {
type: "array",
uniqueItems: true,
items: {
type: "string"
}
},
exceptionPatterns: {
type: "array",
uniqueItems: true,
items: {
type: "string"
}
},
properties: {
enum: ["always", "never"]
}
},
additionalProperties: false
}
],
messages: {
tooShort: "Identifier name '{{name}}' is too short (< {{min}}).",
tooShortPrivate: "Identifier name '#{{name}}' is too short (< {{min}}).",
tooLong: "Identifier name '{{name}}' is too long (> {{max}}).",
tooLongPrivate: "Identifier name #'{{name}}' is too long (> {{max}})."
}
},
create(context) {
const options = context.options[0] || {};
const minLength = typeof options.min !== "undefined" ? options.min : 2;
const maxLength = typeof options.max !== "undefined" ? options.max : Infinity;
const properties = options.properties !== "never";
const exceptions = new Set(options.exceptions);
const exceptionPatterns = (options.exceptionPatterns || []).map(pattern => new RegExp(pattern, "u"));
const reportedNodes = new Set();
/**
* Checks if a string matches the provided exception patterns
* @param {string} name The string to check.
* @returns {boolean} if the string is a match
* @private
*/
function matchesExceptionPattern(name) {
return exceptionPatterns.some(pattern => pattern.test(name));
}
const SUPPORTED_EXPRESSIONS = {
MemberExpression: properties && function(parent) {
return !parent.computed && (
// regular property assignment
(parent.parent.left === parent && parent.parent.type === "AssignmentExpression" ||
// or the last identifier in an ObjectPattern destructuring
parent.parent.type === "Property" && parent.parent.value === parent &&
parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent)
);
},
AssignmentPattern(parent, node) {
return parent.left === node;
},
VariableDeclarator(parent, node) {
return parent.id === node;
},
Property(parent, node) {
if (parent.parent.type === "ObjectPattern") {
const isKeyAndValueSame = parent.value.name === parent.key.name;
return (
!isKeyAndValueSame && parent.value === node ||
isKeyAndValueSame && parent.key === node && properties
);
}
return properties && !parent.computed && parent.key.name === node.name;
},
ImportDefaultSpecifier: true,
RestElement: true,
FunctionExpression: true,
ArrowFunctionExpression: true,
ClassDeclaration: true,
FunctionDeclaration: true,
MethodDefinition: true,
PropertyDefinition: true,
CatchClause: true,
ArrayPattern: true
};
return {
[[
"Identifier",
"PrivateIdentifier"
]](node) {
const name = node.name;
const parent = node.parent;
const nameLength = getStringLength(name);
const isShort = nameLength < minLength;
const isLong = nameLength > maxLength;
if (!(isShort || isLong) || exceptions.has(name) || matchesExceptionPattern(name)) {
return; // Nothing to report
}
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
/*
* We used the range instead of the node because it's possible
* for the same identifier to be represented by two different
* nodes, with the most clear example being shorthand properties:
* { foo }
* In this case, "foo" is represented by one node for the name
* and one for the value. The only way to know they are the same
* is to look at the range.
*/
if (isValidExpression && !reportedNodes.has(node.range.toString()) && (isValidExpression === true || isValidExpression(parent, node))) {
reportedNodes.add(node.range.toString());
let messageId = isShort ? "tooShort" : "tooLong";
if (node.type === "PrivateIdentifier") {
messageId += "Private";
}
context.report({
node,
messageId,
data: { name, min: minLength, max: maxLength }
});
}
}
};
}
};