@dereekb/util 13.11.2 → 13.11.3
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/eslint/index.cjs.default.js +1 -0
- package/eslint/index.cjs.js +607 -0
- package/eslint/index.cjs.mjs +2 -0
- package/eslint/index.d.ts +1 -0
- package/eslint/index.esm.js +603 -0
- package/eslint/package.json +23 -0
- package/eslint/src/index.d.ts +1 -0
- package/eslint/src/lib/comments.d.ts +54 -0
- package/eslint/src/lib/index.d.ts +3 -0
- package/eslint/src/lib/plugin.d.ts +18 -0
- package/eslint/src/lib/prefer-no-side-effects-in-jsdoc.rule.d.ts +42 -0
- package/eslint/src/lib/require-no-side-effects.rule.d.ts +54 -0
- package/fetch/package.json +2 -2
- package/index.cjs.js +1203 -23
- package/index.esm.js +1203 -23
- package/package.json +7 -1
- package/src/lib/array/array.factory.d.ts +2 -0
- package/src/lib/array/array.filter.d.ts +50 -17
- package/src/lib/array/array.find.d.ts +1 -0
- package/src/lib/array/array.index.d.ts +7 -0
- package/src/lib/array/array.indexed.d.ts +21 -0
- package/src/lib/array/array.make.d.ts +7 -0
- package/src/lib/array/array.random.d.ts +1 -0
- package/src/lib/array/array.unique.d.ts +3 -0
- package/src/lib/array/array.value.d.ts +7 -0
- package/src/lib/auth/auth.role.claims.d.ts +7 -0
- package/src/lib/boolean.d.ts +1 -0
- package/src/lib/contact/random.d.ts +14 -0
- package/src/lib/date/date.d.ts +6 -0
- package/src/lib/date/time.d.ts +7 -0
- package/src/lib/date/week.d.ts +7 -0
- package/src/lib/error/error.d.ts +7 -0
- package/src/lib/filter/filter.d.ts +7 -0
- package/src/lib/function/function.boolean.d.ts +7 -0
- package/src/lib/function/function.forward.d.ts +14 -0
- package/src/lib/getter/getter.cache.d.ts +7 -0
- package/src/lib/getter/getter.d.ts +34 -0
- package/src/lib/getter/getter.map.d.ts +7 -0
- package/src/lib/getter/getter.util.d.ts +7 -0
- package/src/lib/grouping.d.ts +8 -0
- package/src/lib/hash.d.ts +1 -0
- package/src/lib/key.d.ts +16 -0
- package/src/lib/map/map.key.d.ts +14 -0
- package/src/lib/model/id.batch.d.ts +7 -0
- package/src/lib/model/id.factory.d.ts +7 -0
- package/src/lib/model/model.conversion.d.ts +35 -0
- package/src/lib/model/model.copy.d.ts +7 -0
- package/src/lib/model/model.d.ts +19 -0
- package/src/lib/model/model.modify.d.ts +14 -0
- package/src/lib/nodejs/stream.d.ts +7 -0
- package/src/lib/number/bound.d.ts +3 -0
- package/src/lib/number/dollar.d.ts +7 -0
- package/src/lib/number/factory.d.ts +7 -0
- package/src/lib/number/random.d.ts +1 -0
- package/src/lib/number/round.d.ts +22 -0
- package/src/lib/number/sort.d.ts +7 -0
- package/src/lib/number/transform.d.ts +7 -0
- package/src/lib/object/object.array.delta.d.ts +7 -0
- package/src/lib/object/object.equal.d.ts +7 -0
- package/src/lib/object/object.filter.pojo.d.ts +87 -0
- package/src/lib/object/object.filter.tuple.d.ts +16 -0
- package/src/lib/object/object.key.d.ts +14 -0
- package/src/lib/object/object.map.d.ts +14 -0
- package/src/lib/path/path.d.ts +9 -0
- package/src/lib/promise/promise.d.ts +21 -0
- package/src/lib/promise/promise.factory.d.ts +7 -0
- package/src/lib/promise/promise.task.d.ts +7 -0
- package/src/lib/service/handler.config.d.ts +28 -0
- package/src/lib/service/handler.d.ts +14 -0
- package/src/lib/set/set.d.ts +21 -0
- package/src/lib/set/set.decision.d.ts +7 -0
- package/src/lib/set/set.delta.d.ts +7 -0
- package/src/lib/set/set.selection.d.ts +7 -0
- package/src/lib/sort.d.ts +8 -0
- package/src/lib/string/char.d.ts +7 -0
- package/src/lib/string/dencoder.d.ts +35 -0
- package/src/lib/string/factory.d.ts +22 -1
- package/src/lib/string/replace.d.ts +78 -0
- package/src/lib/string/search.d.ts +7 -0
- package/src/lib/string/sort.d.ts +7 -0
- package/src/lib/string/string.d.ts +1 -0
- package/src/lib/string/transform.d.ts +53 -0
- package/src/lib/string/tree.d.ts +7 -0
- package/src/lib/string/url.d.ts +7 -0
- package/src/lib/tree/tree.array.d.ts +1 -0
- package/src/lib/tree/tree.explore.d.ts +3 -0
- package/src/lib/type.d.ts +3 -2
- package/src/lib/value/bound.d.ts +28 -0
- package/src/lib/value/comparator.d.ts +16 -0
- package/src/lib/value/decision.d.ts +5 -0
- package/src/lib/value/equal.d.ts +2 -0
- package/src/lib/value/indexed.d.ts +127 -0
- package/src/lib/value/map.d.ts +22 -0
- package/src/lib/value/maybe.type.d.ts +2 -2
- package/src/lib/value/modifier.d.ts +13 -0
- package/src/lib/value/point.d.ts +56 -0
- package/src/lib/value/use.d.ts +37 -0
- package/src/lib/value/vector.d.ts +7 -0
- package/test/index.cjs.js +17 -4
- package/test/index.esm.js +17 -4
- package/test/package.json +2 -2
- package/test/src/lib/shared/shared.fail.d.ts +24 -5
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundler hint string that marks a function call as side-effect-free.
|
|
3
|
+
*/ var NO_SIDE_EFFECTS_TAG = '@__NO_SIDE_EFFECTS__';
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the given comment text contains the @__NO_SIDE_EFFECTS__ marker.
|
|
6
|
+
*
|
|
7
|
+
* @param text - The comment body text (without the `/*` and `*\/` delimiters).
|
|
8
|
+
* @returns True when the marker substring is present.
|
|
9
|
+
*/ function commentContainsNoSideEffects(text) {
|
|
10
|
+
return text.includes(NO_SIDE_EFFECTS_TAG);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the leading whitespace (column indent) of the line containing the given offset.
|
|
14
|
+
*
|
|
15
|
+
* @param sourceText - The full source text being inspected.
|
|
16
|
+
* @param offset - A character offset into `sourceText` indicating the line of interest.
|
|
17
|
+
* @returns The whitespace prefix (spaces/tabs) of that line.
|
|
18
|
+
*/ function getLineIndent(sourceText, offset) {
|
|
19
|
+
var lineStart = offset;
|
|
20
|
+
while(lineStart > 0 && sourceText.charAt(lineStart - 1) !== '\n'){
|
|
21
|
+
lineStart -= 1;
|
|
22
|
+
}
|
|
23
|
+
var cursor = lineStart;
|
|
24
|
+
while(cursor < sourceText.length && (sourceText.charAt(cursor) === ' ' || sourceText.charAt(cursor) === '\t')){
|
|
25
|
+
cursor += 1;
|
|
26
|
+
}
|
|
27
|
+
return sourceText.slice(lineStart, cursor);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the outermost statement node for a FunctionDeclaration — its `ExportNamedDeclaration`
|
|
31
|
+
* or `ExportDefaultDeclaration` parent if exported, otherwise the declaration itself. This is the
|
|
32
|
+
* node ESLint attaches leading comments to.
|
|
33
|
+
*
|
|
34
|
+
* @param node - The FunctionDeclaration AST node.
|
|
35
|
+
* @returns The statement node ESLint attaches leading comments to.
|
|
36
|
+
*/ function getStatementAnchor(node) {
|
|
37
|
+
return node.parent && (node.parent.type === 'ExportNamedDeclaration' || node.parent.type === 'ExportDefaultDeclaration') ? node.parent : node;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the statement is an overload signature (TSDeclareFunction) sharing the given name.
|
|
41
|
+
*
|
|
42
|
+
* @param stmt - The statement node to check (may be an export wrapper).
|
|
43
|
+
* @param name - The function identifier name to match.
|
|
44
|
+
* @returns True when `stmt` is an overload signature for `name`.
|
|
45
|
+
*/ function isOverloadSignature(stmt, name) {
|
|
46
|
+
var _inner_id;
|
|
47
|
+
var inner = stmt.type === 'ExportNamedDeclaration' || stmt.type === 'ExportDefaultDeclaration' ? stmt.declaration : stmt;
|
|
48
|
+
return (inner === null || inner === void 0 ? void 0 : inner.type) === 'TSDeclareFunction' && ((_inner_id = inner.id) === null || _inner_id === void 0 ? void 0 : _inner_id.type) === 'Identifier' && inner.id.name === name;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Walks backward from the implementation FunctionDeclaration through any overload signatures
|
|
52
|
+
* with the same name, collecting:
|
|
53
|
+
*
|
|
54
|
+
* - The leading JSDoc block (preferring the one attached to the **first** overload, since that's
|
|
55
|
+
* where the function's documentation conventionally lives).
|
|
56
|
+
* - All orphan `@__NO_SIDE_EFFECTS__` line/block comments encountered between overloads or
|
|
57
|
+
* between the last overload and the implementation.
|
|
58
|
+
*
|
|
59
|
+
* This handles the common pattern:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* \/\*\* doc \*\/
|
|
63
|
+
* export function foo(a: number): number;
|
|
64
|
+
* export function foo(a: string): string;
|
|
65
|
+
* \/\/ \@__NO_SIDE_EFFECTS__
|
|
66
|
+
* export function foo(a: any) { ... }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @param sourceCode - The ESLint `SourceCode` object used to read leading comments.
|
|
70
|
+
* @param implNode - The implementation FunctionDeclaration node.
|
|
71
|
+
* @returns The leading JSDoc (if any) and any orphan side-effect annotation comments.
|
|
72
|
+
*/ function findFunctionLeadingContext(sourceCode, implNode) {
|
|
73
|
+
var _implNode_id;
|
|
74
|
+
if (((_implNode_id = implNode.id) === null || _implNode_id === void 0 ? void 0 : _implNode_id.type) !== 'Identifier') {
|
|
75
|
+
return {
|
|
76
|
+
jsdoc: null,
|
|
77
|
+
orphanLineComments: []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var name = implNode.id.name;
|
|
81
|
+
var implStmt = getStatementAnchor(implNode);
|
|
82
|
+
var container = implStmt.parent;
|
|
83
|
+
var chainStartIdx = -1;
|
|
84
|
+
var implIdx = -1;
|
|
85
|
+
if (container && Array.isArray(container.body)) {
|
|
86
|
+
implIdx = container.body.indexOf(implStmt);
|
|
87
|
+
if (implIdx >= 0) {
|
|
88
|
+
chainStartIdx = implIdx;
|
|
89
|
+
for(var i = implIdx - 1; i >= 0; i -= 1){
|
|
90
|
+
if (isOverloadSignature(container.body[i], name)) {
|
|
91
|
+
chainStartIdx = i;
|
|
92
|
+
} else {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var firstJsdoc = null;
|
|
99
|
+
var anyJsdocHasNoSideEffects = false;
|
|
100
|
+
var orphanLineComments = [];
|
|
101
|
+
function processCommentsForStatement(stmt) {
|
|
102
|
+
var comments = sourceCode.getCommentsBefore(stmt) || [];
|
|
103
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
104
|
+
try {
|
|
105
|
+
for(var _iterator = comments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
106
|
+
var comment = _step.value;
|
|
107
|
+
if (comment.type === 'Block' && comment.value.startsWith('*')) {
|
|
108
|
+
var hasMarker = commentContainsNoSideEffects(comment.value);
|
|
109
|
+
if (hasMarker) {
|
|
110
|
+
anyJsdocHasNoSideEffects = true;
|
|
111
|
+
}
|
|
112
|
+
// Auto-fix target: the first JSDoc in the chain (where the function's docs conventionally live).
|
|
113
|
+
if (!firstJsdoc) {
|
|
114
|
+
firstJsdoc = {
|
|
115
|
+
node: comment,
|
|
116
|
+
text: comment.value,
|
|
117
|
+
hasNoSideEffects: hasMarker
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
} else if (commentContainsNoSideEffects(comment.value)) {
|
|
121
|
+
orphanLineComments.push(comment);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
_didIteratorError = true;
|
|
126
|
+
_iteratorError = err;
|
|
127
|
+
} finally{
|
|
128
|
+
try {
|
|
129
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
130
|
+
_iterator.return();
|
|
131
|
+
}
|
|
132
|
+
} finally{
|
|
133
|
+
if (_didIteratorError) {
|
|
134
|
+
throw _iteratorError;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (chainStartIdx >= 0 && implIdx >= 0) {
|
|
140
|
+
for(var i1 = chainStartIdx; i1 <= implIdx; i1 += 1){
|
|
141
|
+
processCommentsForStatement(container.body[i1]);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Fallback: no container body found; just look at comments before the implementation.
|
|
145
|
+
processCommentsForStatement(implStmt);
|
|
146
|
+
}
|
|
147
|
+
// Report the first JSDoc as the canonical jsdoc, but reflect any-in-chain satisfaction
|
|
148
|
+
// so callers don't re-annotate when the marker is already present elsewhere in the chain.
|
|
149
|
+
var resolved = null;
|
|
150
|
+
var captured = firstJsdoc;
|
|
151
|
+
if (captured) {
|
|
152
|
+
resolved = {
|
|
153
|
+
node: captured.node,
|
|
154
|
+
text: captured.text,
|
|
155
|
+
hasNoSideEffects: anyJsdocHasNoSideEffects
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
jsdoc: resolved,
|
|
160
|
+
orphanLineComments: orphanLineComments
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _array_like_to_array$1(arr, len) {
|
|
165
|
+
if (len == null || len > arr.length) len = arr.length;
|
|
166
|
+
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
167
|
+
return arr2;
|
|
168
|
+
}
|
|
169
|
+
function _array_with_holes$1(arr) {
|
|
170
|
+
if (Array.isArray(arr)) return arr;
|
|
171
|
+
}
|
|
172
|
+
function _array_without_holes(arr) {
|
|
173
|
+
if (Array.isArray(arr)) return _array_like_to_array$1(arr);
|
|
174
|
+
}
|
|
175
|
+
function _iterable_to_array(iter) {
|
|
176
|
+
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
|
|
177
|
+
}
|
|
178
|
+
function _iterable_to_array_limit$1(arr, i) {
|
|
179
|
+
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
|
|
180
|
+
if (_i == null) return;
|
|
181
|
+
var _arr = [];
|
|
182
|
+
var _n = true;
|
|
183
|
+
var _d = false;
|
|
184
|
+
var _s, _e;
|
|
185
|
+
try {
|
|
186
|
+
for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
|
|
187
|
+
_arr.push(_s.value);
|
|
188
|
+
if (i && _arr.length === i) break;
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
_d = true;
|
|
192
|
+
_e = err;
|
|
193
|
+
} finally{
|
|
194
|
+
try {
|
|
195
|
+
if (!_n && _i["return"] != null) _i["return"]();
|
|
196
|
+
} finally{
|
|
197
|
+
if (_d) throw _e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return _arr;
|
|
201
|
+
}
|
|
202
|
+
function _non_iterable_rest$1() {
|
|
203
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
204
|
+
}
|
|
205
|
+
function _non_iterable_spread() {
|
|
206
|
+
throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
207
|
+
}
|
|
208
|
+
function _sliced_to_array$1(arr, i) {
|
|
209
|
+
return _array_with_holes$1(arr) || _iterable_to_array_limit$1(arr, i) || _unsupported_iterable_to_array$1(arr, i) || _non_iterable_rest$1();
|
|
210
|
+
}
|
|
211
|
+
function _to_consumable_array(arr) {
|
|
212
|
+
return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array$1(arr) || _non_iterable_spread();
|
|
213
|
+
}
|
|
214
|
+
function _unsupported_iterable_to_array$1(o, minLen) {
|
|
215
|
+
if (!o) return;
|
|
216
|
+
if (typeof o === "string") return _array_like_to_array$1(o, minLen);
|
|
217
|
+
var n = Object.prototype.toString.call(o).slice(8, -1);
|
|
218
|
+
if (n === "Object" && o.constructor) n = o.constructor.name;
|
|
219
|
+
if (n === "Map" || n === "Set") return Array.from(n);
|
|
220
|
+
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$1(o, minLen);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The JSDoc tag identifying a function as a factory in the @dereekb conventions.
|
|
224
|
+
*/ var FACTORY_JSDOC_TAG = '@dbxUtilKind factory';
|
|
225
|
+
/**
|
|
226
|
+
* Default suffix patterns considered factory-like by name when `checkNamePatterns` is enabled.
|
|
227
|
+
*/ var DEFAULT_NAME_PATTERNS = [
|
|
228
|
+
/(?:Factory|Factories|Service|Services|Function|Functions)$/,
|
|
229
|
+
/^(?:make|build|create)[A-Z]/,
|
|
230
|
+
/^(?:firestore|optionalFirestore)[A-Z]/
|
|
231
|
+
];
|
|
232
|
+
/**
|
|
233
|
+
* Returns the function's identifier name, or null if anonymous.
|
|
234
|
+
*
|
|
235
|
+
* @param node - The FunctionDeclaration AST node.
|
|
236
|
+
* @returns The function's identifier name, or `null` for anonymous functions.
|
|
237
|
+
*/ function getFunctionName(node) {
|
|
238
|
+
var _node_id;
|
|
239
|
+
if (((_node_id = node.id) === null || _node_id === void 0 ? void 0 : _node_id.type) === 'Identifier') {
|
|
240
|
+
return node.id.name;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Builds the merged set of name patterns based on rule options.
|
|
246
|
+
*
|
|
247
|
+
* @param options - The resolved rule options.
|
|
248
|
+
* @returns The combined default + additional regex patterns, or an empty list when name-pattern matching is disabled.
|
|
249
|
+
*/ function buildNamePatterns(options) {
|
|
250
|
+
var _options_additionalNamePatterns;
|
|
251
|
+
if (!options.checkNamePatterns) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
var additional = ((_options_additionalNamePatterns = options.additionalNamePatterns) !== null && _options_additionalNamePatterns !== void 0 ? _options_additionalNamePatterns : []).map(function(source) {
|
|
255
|
+
return new RegExp(source);
|
|
256
|
+
});
|
|
257
|
+
return _to_consumable_array(DEFAULT_NAME_PATTERNS).concat(_to_consumable_array(additional));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* ESLint rule requiring the side-effect-free annotation inside the JSDoc of every
|
|
261
|
+
* factory function — that is, declarations carrying the dbxUtilKind factory JSDoc tag,
|
|
262
|
+
* and optionally functions matching factory name patterns.
|
|
263
|
+
*
|
|
264
|
+
* Auto-fix inserts the annotation as the last line of the JSDoc, removes any
|
|
265
|
+
* redundant standalone-comment annotation between the JSDoc and the declaration,
|
|
266
|
+
* and (when matched by name with no JSDoc) creates a minimal JSDoc carrying both tags.
|
|
267
|
+
*/ var utilRequireNoSideEffectsRule = {
|
|
268
|
+
meta: {
|
|
269
|
+
type: 'suggestion',
|
|
270
|
+
fixable: 'code',
|
|
271
|
+
docs: {
|
|
272
|
+
description: 'Require @__NO_SIDE_EFFECTS__ inside the JSDoc block of factory functions so esbuild can drop unused calls during tree-shaking.',
|
|
273
|
+
recommended: true
|
|
274
|
+
},
|
|
275
|
+
messages: {
|
|
276
|
+
missingNoSideEffectsJsdoc: 'Factory function "{{name}}" is missing the `@__NO_SIDE_EFFECTS__` annotation in its JSDoc. Add it as the last tag inside the JSDoc block so esbuild can drop unused calls during tree-shaking.',
|
|
277
|
+
missingJsdocForFactory: 'Factory-named function "{{name}}" has no JSDoc block. Add a JSDoc block containing `@__NO_SIDE_EFFECTS__` so esbuild can drop unused calls during tree-shaking.'
|
|
278
|
+
},
|
|
279
|
+
schema: [
|
|
280
|
+
{
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
checkNamePatterns: {
|
|
284
|
+
type: 'boolean',
|
|
285
|
+
description: 'Also flag functions whose names match factory naming patterns, in addition to JSDoc-tag detection.'
|
|
286
|
+
},
|
|
287
|
+
additionalNamePatterns: {
|
|
288
|
+
type: 'array',
|
|
289
|
+
items: {
|
|
290
|
+
type: 'string'
|
|
291
|
+
},
|
|
292
|
+
description: 'Additional name pattern source strings to treat as factory signals when checkNamePatterns is true.'
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
additionalProperties: false
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
create: function create(context) {
|
|
300
|
+
var _context_options_;
|
|
301
|
+
var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
|
|
302
|
+
var namePatterns = buildNamePatterns(options);
|
|
303
|
+
var sourceCode = context.sourceCode;
|
|
304
|
+
var sourceText = sourceCode.getText();
|
|
305
|
+
function nameMatchesFactoryPattern(name) {
|
|
306
|
+
return namePatterns.some(function(pattern) {
|
|
307
|
+
return pattern.test(name);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function checkFunction(node) {
|
|
311
|
+
var _jsdoc_text;
|
|
312
|
+
// Skip overload signatures (TSDeclareFunction) and bodyless declarations.
|
|
313
|
+
if (node.type !== 'FunctionDeclaration' || !node.body) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
var name = getFunctionName(node);
|
|
317
|
+
if (!name) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Walks the overload chain (if any) so we read the JSDoc on the first overload
|
|
321
|
+
// and any orphan annotations placed between overloads or above the implementation.
|
|
322
|
+
var _findFunctionLeadingContext = findFunctionLeadingContext(sourceCode, node), jsdoc = _findFunctionLeadingContext.jsdoc, redundantLineComments = _findFunctionLeadingContext.orphanLineComments;
|
|
323
|
+
var taggedAsFactory = (jsdoc === null || jsdoc === void 0 ? void 0 : (_jsdoc_text = jsdoc.text) === null || _jsdoc_text === void 0 ? void 0 : _jsdoc_text.includes(FACTORY_JSDOC_TAG)) === true;
|
|
324
|
+
var matchedByName = !taggedAsFactory && namePatterns.length > 0 && nameMatchesFactoryPattern(name);
|
|
325
|
+
if (!taggedAsFactory && !matchedByName) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Already annotated inside the JSDoc — passing.
|
|
329
|
+
if (jsdoc === null || jsdoc === void 0 ? void 0 : jsdoc.hasNoSideEffects) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
var messageId = jsdoc ? 'missingNoSideEffectsJsdoc' : 'missingJsdocForFactory';
|
|
333
|
+
context.report({
|
|
334
|
+
node: node.id,
|
|
335
|
+
messageId: messageId,
|
|
336
|
+
data: {
|
|
337
|
+
name: name
|
|
338
|
+
},
|
|
339
|
+
fix: function fix(fixer) {
|
|
340
|
+
var fixes = [];
|
|
341
|
+
if (jsdoc) {
|
|
342
|
+
var jsdocText = jsdoc.text; // text excludes /* and */
|
|
343
|
+
var jsdocStart = jsdoc.node.range[0];
|
|
344
|
+
var jsdocEnd = jsdoc.node.range[1];
|
|
345
|
+
// Determine the column the JSDoc starts at to align the new line.
|
|
346
|
+
var jsdocIndent = getLineIndent(sourceText, jsdocStart);
|
|
347
|
+
// If the JSDoc is single-line (e.g. `/** @dbxUtilKind factory */`),
|
|
348
|
+
// expand it to multi-line. Detect by absence of newline in the body.
|
|
349
|
+
if (!jsdocText.includes('\n')) {
|
|
350
|
+
var bodyTrimmed = jsdocText.replace(/^\*\s*/, '').replace(/\s*$/, '');
|
|
351
|
+
var newBody = "/**\n".concat(jsdocIndent, " * ").concat(bodyTrimmed, "\n").concat(jsdocIndent, " * ").concat(NO_SIDE_EFFECTS_TAG, "\n").concat(jsdocIndent, " */");
|
|
352
|
+
fixes.push(fixer.replaceTextRange([
|
|
353
|
+
jsdocStart,
|
|
354
|
+
jsdocEnd
|
|
355
|
+
], newBody));
|
|
356
|
+
} else {
|
|
357
|
+
|
|
358
|
+
// immediately before the line containing the closing `*/`, so the closing line
|
|
359
|
+
// and existing body lines remain untouched.
|
|
360
|
+
var closingMarkerStart = jsdocEnd - 2; // start of `*/`
|
|
361
|
+
var closingLineStart = closingMarkerStart;
|
|
362
|
+
while(closingLineStart > 0 && sourceText.charAt(closingLineStart - 1) !== '\n'){
|
|
363
|
+
closingLineStart -= 1;
|
|
364
|
+
}
|
|
365
|
+
var insertion = "".concat(jsdocIndent, " * ").concat(NO_SIDE_EFFECTS_TAG, "\n");
|
|
366
|
+
fixes.push(fixer.insertTextBeforeRange([
|
|
367
|
+
closingLineStart,
|
|
368
|
+
closingLineStart
|
|
369
|
+
], insertion));
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// No JSDoc — create one above the function declaration with matching indent.
|
|
373
|
+
// Use the function's leading position. Account for `export` keyword: getCommentsBefore
|
|
374
|
+
// attaches to the declaration including its modifiers, so node.range[0] is correct.
|
|
375
|
+
var nodeStart = node.range[0];
|
|
376
|
+
var indent = getLineIndent(sourceText, nodeStart);
|
|
377
|
+
var newJsdoc = "/**\n".concat(indent, " * @dbxUtilKind factory\n").concat(indent, " * ").concat(NO_SIDE_EFFECTS_TAG, "\n").concat(indent, " */\n").concat(indent);
|
|
378
|
+
fixes.push(fixer.insertTextBeforeRange([
|
|
379
|
+
nodeStart,
|
|
380
|
+
nodeStart
|
|
381
|
+
], newJsdoc));
|
|
382
|
+
}
|
|
383
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
384
|
+
try {
|
|
385
|
+
// Remove any redundant adjacent annotation comments now that JSDoc carries the tag.
|
|
386
|
+
for(var _iterator = redundantLineComments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
387
|
+
var redundant = _step.value;
|
|
388
|
+
var _redundant_range = _sliced_to_array$1(redundant.range, 2), start = _redundant_range[0], end = _redundant_range[1];
|
|
389
|
+
// Extend the removal to include the trailing newline + indent so we don't leave a blank line.
|
|
390
|
+
var removeEnd = end;
|
|
391
|
+
while(removeEnd < sourceText.length && (sourceText.charAt(removeEnd) === ' ' || sourceText.charAt(removeEnd) === '\t')){
|
|
392
|
+
removeEnd += 1;
|
|
393
|
+
}
|
|
394
|
+
if (sourceText.charAt(removeEnd) === '\n') {
|
|
395
|
+
removeEnd += 1;
|
|
396
|
+
}
|
|
397
|
+
// Also drop leading indent on the comment's line.
|
|
398
|
+
var removeStart = start;
|
|
399
|
+
while(removeStart > 0 && (sourceText.charAt(removeStart - 1) === ' ' || sourceText.charAt(removeStart - 1) === '\t')){
|
|
400
|
+
removeStart -= 1;
|
|
401
|
+
}
|
|
402
|
+
fixes.push(fixer.removeRange([
|
|
403
|
+
removeStart,
|
|
404
|
+
removeEnd
|
|
405
|
+
]));
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
_didIteratorError = true;
|
|
409
|
+
_iteratorError = err;
|
|
410
|
+
} finally{
|
|
411
|
+
try {
|
|
412
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
413
|
+
_iterator.return();
|
|
414
|
+
}
|
|
415
|
+
} finally{
|
|
416
|
+
if (_didIteratorError) {
|
|
417
|
+
throw _iteratorError;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return fixes;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
FunctionDeclaration: checkFunction
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
function _array_like_to_array(arr, len) {
|
|
432
|
+
if (len == null || len > arr.length) len = arr.length;
|
|
433
|
+
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
434
|
+
return arr2;
|
|
435
|
+
}
|
|
436
|
+
function _array_with_holes(arr) {
|
|
437
|
+
if (Array.isArray(arr)) return arr;
|
|
438
|
+
}
|
|
439
|
+
function _iterable_to_array_limit(arr, i) {
|
|
440
|
+
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
|
|
441
|
+
if (_i == null) return;
|
|
442
|
+
var _arr = [];
|
|
443
|
+
var _n = true;
|
|
444
|
+
var _d = false;
|
|
445
|
+
var _s, _e;
|
|
446
|
+
try {
|
|
447
|
+
for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
|
|
448
|
+
_arr.push(_s.value);
|
|
449
|
+
if (i && _arr.length === i) break;
|
|
450
|
+
}
|
|
451
|
+
} catch (err) {
|
|
452
|
+
_d = true;
|
|
453
|
+
_e = err;
|
|
454
|
+
} finally{
|
|
455
|
+
try {
|
|
456
|
+
if (!_n && _i["return"] != null) _i["return"]();
|
|
457
|
+
} finally{
|
|
458
|
+
if (_d) throw _e;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return _arr;
|
|
462
|
+
}
|
|
463
|
+
function _non_iterable_rest() {
|
|
464
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
465
|
+
}
|
|
466
|
+
function _sliced_to_array(arr, i) {
|
|
467
|
+
return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
|
|
468
|
+
}
|
|
469
|
+
function _unsupported_iterable_to_array(o, minLen) {
|
|
470
|
+
if (!o) return;
|
|
471
|
+
if (typeof o === "string") return _array_like_to_array(o, minLen);
|
|
472
|
+
var n = Object.prototype.toString.call(o).slice(8, -1);
|
|
473
|
+
if (n === "Object" && o.constructor) n = o.constructor.name;
|
|
474
|
+
if (n === "Map" || n === "Set") return Array.from(n);
|
|
475
|
+
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
|
|
476
|
+
}
|
|
477
|
+
var utilPreferNoSideEffectsInJsdocRule = {
|
|
478
|
+
meta: {
|
|
479
|
+
type: 'suggestion',
|
|
480
|
+
fixable: 'code',
|
|
481
|
+
docs: {
|
|
482
|
+
description: "Prefer the @__NO_SIDE_EFFECTS__ annotation inside a function's JSDoc instead of a separate line comment between the JSDoc and the declaration.",
|
|
483
|
+
recommended: true
|
|
484
|
+
},
|
|
485
|
+
messages: {
|
|
486
|
+
preferJsdocPlacement: 'Move the `@__NO_SIDE_EFFECTS__` annotation into the JSDoc block of "{{name}}" (as the last tag before the closing) instead of a separate line comment.'
|
|
487
|
+
},
|
|
488
|
+
schema: []
|
|
489
|
+
},
|
|
490
|
+
create: function create(context) {
|
|
491
|
+
var sourceCode = context.sourceCode;
|
|
492
|
+
var sourceText = sourceCode.getText();
|
|
493
|
+
function checkFunction(node) {
|
|
494
|
+
var _node_id;
|
|
495
|
+
if (node.type !== 'FunctionDeclaration' || !node.body) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (((_node_id = node.id) === null || _node_id === void 0 ? void 0 : _node_id.type) !== 'Identifier') {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
var name = node.id.name;
|
|
502
|
+
// Walks the overload chain (if any) so we find the JSDoc on the first overload
|
|
503
|
+
// and any orphan annotations between overloads or above the implementation.
|
|
504
|
+
var _findFunctionLeadingContext = findFunctionLeadingContext(sourceCode, node), jsdoc = _findFunctionLeadingContext.jsdoc, orphanLineComments = _findFunctionLeadingContext.orphanLineComments;
|
|
505
|
+
// Only flag when both signals are present: a JSDoc to absorb the tag, AND an orphan annotation.
|
|
506
|
+
if (!jsdoc || orphanLineComments.length === 0) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
context.report({
|
|
510
|
+
node: node.id,
|
|
511
|
+
messageId: 'preferJsdocPlacement',
|
|
512
|
+
data: {
|
|
513
|
+
name: name
|
|
514
|
+
},
|
|
515
|
+
fix: function fix(fixer) {
|
|
516
|
+
var fixes = [];
|
|
517
|
+
var jsdocText = jsdoc.text;
|
|
518
|
+
var jsdocStart = jsdoc.node.range[0];
|
|
519
|
+
var jsdocEnd = jsdoc.node.range[1];
|
|
520
|
+
var jsdocIndent = getLineIndent(sourceText, jsdocStart);
|
|
521
|
+
// Insert into JSDoc only if the tag isn't already there (preserve idempotency).
|
|
522
|
+
if (!jsdoc.hasNoSideEffects) {
|
|
523
|
+
if (!jsdocText.includes('\n')) {
|
|
524
|
+
// Single-line JSDoc — expand to multi-line so the new tag has its own line.
|
|
525
|
+
var bodyTrimmed = jsdocText.replace(/^\*\s*/, '').replace(/\s*$/, '');
|
|
526
|
+
var newBody = "/**\n".concat(jsdocIndent, " * ").concat(bodyTrimmed, "\n").concat(jsdocIndent, " * ").concat(NO_SIDE_EFFECTS_TAG, "\n").concat(jsdocIndent, " */");
|
|
527
|
+
fixes.push(fixer.replaceTextRange([
|
|
528
|
+
jsdocStart,
|
|
529
|
+
jsdocEnd
|
|
530
|
+
], newBody));
|
|
531
|
+
} else {
|
|
532
|
+
// Multi-line JSDoc — insert a new tag line immediately before the closing `*/` line.
|
|
533
|
+
var closingMarkerStart = jsdocEnd - 2;
|
|
534
|
+
var closingLineStart = closingMarkerStart;
|
|
535
|
+
while(closingLineStart > 0 && sourceText.charAt(closingLineStart - 1) !== '\n'){
|
|
536
|
+
closingLineStart -= 1;
|
|
537
|
+
}
|
|
538
|
+
var insertion = "".concat(jsdocIndent, " * ").concat(NO_SIDE_EFFECTS_TAG, "\n");
|
|
539
|
+
fixes.push(fixer.insertTextBeforeRange([
|
|
540
|
+
closingLineStart,
|
|
541
|
+
closingLineStart
|
|
542
|
+
], insertion));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
546
|
+
try {
|
|
547
|
+
// Remove the orphan line/block comment annotations.
|
|
548
|
+
for(var _iterator = orphanLineComments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
549
|
+
var orphan = _step.value;
|
|
550
|
+
var _orphan_range = _sliced_to_array(orphan.range, 2), start = _orphan_range[0], end = _orphan_range[1];
|
|
551
|
+
var removeEnd = end;
|
|
552
|
+
while(removeEnd < sourceText.length && (sourceText.charAt(removeEnd) === ' ' || sourceText.charAt(removeEnd) === '\t')){
|
|
553
|
+
removeEnd += 1;
|
|
554
|
+
}
|
|
555
|
+
if (sourceText.charAt(removeEnd) === '\n') {
|
|
556
|
+
removeEnd += 1;
|
|
557
|
+
}
|
|
558
|
+
var removeStart = start;
|
|
559
|
+
while(removeStart > 0 && (sourceText.charAt(removeStart - 1) === ' ' || sourceText.charAt(removeStart - 1) === '\t')){
|
|
560
|
+
removeStart -= 1;
|
|
561
|
+
}
|
|
562
|
+
fixes.push(fixer.removeRange([
|
|
563
|
+
removeStart,
|
|
564
|
+
removeEnd
|
|
565
|
+
]));
|
|
566
|
+
}
|
|
567
|
+
} catch (err) {
|
|
568
|
+
_didIteratorError = true;
|
|
569
|
+
_iteratorError = err;
|
|
570
|
+
} finally{
|
|
571
|
+
try {
|
|
572
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
573
|
+
_iterator.return();
|
|
574
|
+
}
|
|
575
|
+
} finally{
|
|
576
|
+
if (_didIteratorError) {
|
|
577
|
+
throw _iteratorError;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return fixes;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
FunctionDeclaration: checkFunction
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* ESLint plugin for @dereekb/util rules.
|
|
593
|
+
*
|
|
594
|
+
* Register as a plugin in your flat ESLint config, then enable individual rules
|
|
595
|
+
* under the chosen plugin prefix (e.g. 'dereekb-util/require-no-side-effects').
|
|
596
|
+
*/ var utilEslintPlugin = {
|
|
597
|
+
rules: {
|
|
598
|
+
'require-no-side-effects': utilRequireNoSideEffectsRule,
|
|
599
|
+
'prefer-no-side-effects-in-jsdoc': utilPreferNoSideEffectsInJsdocRule
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
export { utilEslintPlugin, utilPreferNoSideEffectsInJsdocRule, utilRequireNoSideEffectsRule };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dereekb/util/eslint",
|
|
3
|
+
"version": "13.11.3",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@typescript-eslint/utils": "8.59.0"
|
|
6
|
+
},
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@typescript-eslint/parser": "8.59.0",
|
|
9
|
+
"eslint": "9.39.4"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
".": {
|
|
14
|
+
"module": "./index.esm.js",
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"import": "./index.cjs.mjs",
|
|
17
|
+
"default": "./index.cjs.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"module": "./index.esm.js",
|
|
21
|
+
"main": "./index.cjs.js",
|
|
22
|
+
"types": "./index.d.ts"
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundler hint string that marks a function call as side-effect-free.
|
|
3
|
+
*/
|
|
4
|
+
export declare const NO_SIDE_EFFECTS_TAG = "@__NO_SIDE_EFFECTS__";
|
|
5
|
+
type AstNode = any;
|
|
6
|
+
export interface JsdocCommentInfo {
|
|
7
|
+
readonly node: AstNode;
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly hasNoSideEffects: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface FunctionLeadingContext {
|
|
12
|
+
readonly jsdoc: JsdocCommentInfo | null;
|
|
13
|
+
readonly orphanLineComments: readonly AstNode[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if the given comment text contains the @__NO_SIDE_EFFECTS__ marker.
|
|
17
|
+
*
|
|
18
|
+
* @param text - The comment body text (without the `/*` and `*\/` delimiters).
|
|
19
|
+
* @returns True when the marker substring is present.
|
|
20
|
+
*/
|
|
21
|
+
export declare function commentContainsNoSideEffects(text: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the leading whitespace (column indent) of the line containing the given offset.
|
|
24
|
+
*
|
|
25
|
+
* @param sourceText - The full source text being inspected.
|
|
26
|
+
* @param offset - A character offset into `sourceText` indicating the line of interest.
|
|
27
|
+
* @returns The whitespace prefix (spaces/tabs) of that line.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getLineIndent(sourceText: string, offset: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Walks backward from the implementation FunctionDeclaration through any overload signatures
|
|
32
|
+
* with the same name, collecting:
|
|
33
|
+
*
|
|
34
|
+
* - The leading JSDoc block (preferring the one attached to the **first** overload, since that's
|
|
35
|
+
* where the function's documentation conventionally lives).
|
|
36
|
+
* - All orphan `@__NO_SIDE_EFFECTS__` line/block comments encountered between overloads or
|
|
37
|
+
* between the last overload and the implementation.
|
|
38
|
+
*
|
|
39
|
+
* This handles the common pattern:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* \/\*\* doc \*\/
|
|
43
|
+
* export function foo(a: number): number;
|
|
44
|
+
* export function foo(a: string): string;
|
|
45
|
+
* \/\/ \@__NO_SIDE_EFFECTS__
|
|
46
|
+
* export function foo(a: any) { ... }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @param sourceCode - The ESLint `SourceCode` object used to read leading comments.
|
|
50
|
+
* @param implNode - The implementation FunctionDeclaration node.
|
|
51
|
+
* @returns The leading JSDoc (if any) and any orphan side-effect annotation comments.
|
|
52
|
+
*/
|
|
53
|
+
export declare function findFunctionLeadingContext(sourceCode: AstNode, implNode: AstNode): FunctionLeadingContext;
|
|
54
|
+
export {};
|