xshell 1.0.85 → 1.0.87

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.
Files changed (3) hide show
  1. package/package.json +16 -8
  2. package/xlint.d.ts +3 -0
  3. package/xlint.js +898 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xshell",
3
- "version": "1.0.85",
3
+ "version": "1.0.87",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -53,11 +53,14 @@
53
53
  ]
54
54
  },
55
55
  "dependencies": {
56
- "@babel/core": "^7.24.3",
57
- "@babel/parser": "^7.24.1",
56
+ "@babel/core": "^7.24.4",
57
+ "@babel/parser": "^7.24.4",
58
58
  "@babel/traverse": "^7.24.1",
59
59
  "@koa/cors": "^5.0.0",
60
60
  "@types/ws": "^8.5.10",
61
+ "@typescript-eslint/eslint-plugin": "^7.5.0",
62
+ "@typescript-eslint/parser": "^7.5.0",
63
+ "@typescript-eslint/utils": "^7.5.0",
61
64
  "ali-oss": "^6.20.0",
62
65
  "archiver": "^7.0.1",
63
66
  "byte-size": "^8.1.1",
@@ -68,6 +71,9 @@
68
71
  "colors": "^1.4.0",
69
72
  "commander": "^12.0.0",
70
73
  "emoji-regex": "^10.3.0",
74
+ "eslint": "^9.0.0",
75
+ "eslint-plugin-import": "^2.29.1",
76
+ "eslint-plugin-react": "^7.34.1",
71
77
  "gulp-sort": "^2.0.0",
72
78
  "hash-string": "^1.0.0",
73
79
  "i18next": "^23.10.1",
@@ -87,9 +93,9 @@
87
93
  "through2": "^4.0.2",
88
94
  "tough-cookie": "^4.1.3",
89
95
  "tslib": "^2.6.2",
90
- "typescript": "^5.4.3",
96
+ "typescript": "^5.4.4",
91
97
  "ua-parser-js": "2.0.0-alpha.2",
92
- "undici": "^6.10.1",
98
+ "undici": "^6.11.1",
93
99
  "vinyl": "^3.0.0",
94
100
  "vinyl-fs": "^4.0.0",
95
101
  "ws": "^8.16.0",
@@ -105,19 +111,21 @@
105
111
  "@types/babel__traverse": "^7.20.5",
106
112
  "@types/byte-size": "^8.1.2",
107
113
  "@types/chardet": "^0.8.3",
114
+ "@types/eslint": "^8.56.7",
115
+ "@types/estree": "^1.0.5",
108
116
  "@types/gulp-sort": "2.0.4",
109
117
  "@types/js-cookie": "^3.0.6",
110
118
  "@types/koa": "^2.15.0",
111
119
  "@types/koa-compress": "^4.0.6",
112
120
  "@types/lodash": "^4.17.0",
113
121
  "@types/mime-types": "^2.1.4",
114
- "@types/node": "^20.11.30",
115
- "@types/react": "^18.2.70",
122
+ "@types/node": "^20.12.5",
123
+ "@types/react": "^18.2.74",
116
124
  "@types/through2": "^2.0.41",
117
125
  "@types/tough-cookie": "^4.0.5",
118
126
  "@types/ua-parser-js": "^0.7.39",
119
127
  "@types/vinyl-fs": "^3.0.5",
120
- "@types/vscode": "^1.87.0"
128
+ "@types/vscode": "^1.88.0"
121
129
  },
122
130
  "pnpm": {
123
131
  "patchedDependencies": {
package/xlint.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ESLint, Linter } from 'eslint';
2
+ export declare const xlint_plugin: ESLint.Plugin;
3
+ export declare const xlint_config: Linter.FlatConfig;
package/xlint.js ADDED
@@ -0,0 +1,898 @@
1
+ import { AST_TOKEN_TYPES, ASTUtils, TSESTree } from '@typescript-eslint/utils';
2
+ import TSParser from '@typescript-eslint/parser';
3
+ import ts_plugin from '@typescript-eslint/eslint-plugin';
4
+ import react_plugin from 'eslint-plugin-react';
5
+ import import_plugin from 'eslint-plugin-import';
6
+ const line_break_pattern = /\r\n|[\r\n\u2028\u2029]/u;
7
+ const meta = {
8
+ fixable: 'whitespace',
9
+ type: 'layout',
10
+ };
11
+ const UNIONS = ['|', '&'];
12
+ // 用 ast explorer 选 esprima 来看
13
+ // node.loc.start.line 下标从 1 开始
14
+ export const xlint_plugin = {
15
+ rules: {
16
+ 'fold-jsdoc-comments': {
17
+ meta,
18
+ create: context => ({
19
+ Program() {
20
+ for (const comment of context.sourceCode.getAllComments())
21
+ if (comment.type === 'Block') {
22
+ const lines = comment.value.split(line_break_pattern);
23
+ if (lines.length >= 3 &&
24
+ /^\*\s*$/u.test(lines[0]) &&
25
+ lines.slice(1, -1).every(line => /^\s*\*/u.test(line)) &&
26
+ /^[\s\*]*$/u.test(lines.at(-1)))
27
+ context.report({
28
+ loc: {
29
+ start: comment.loc.start,
30
+ end: comment.loc.end
31
+ },
32
+ message: 'Redundant jsdoc comment.',
33
+ fix(fixer) {
34
+ const indent = ' '.repeat(comment.loc.start.column);
35
+ const indenter = (line) => line.replace(/^\s*\*\s?/u, `${indent} `);
36
+ return fixer.replaceTextRange(comment.range, lines.length === 3 ?
37
+ `/** ${lines[1].replace(/^\s*\*\s*/u, '')} */`
38
+ :
39
+ [
40
+ `/** ${lines[1].replace(/^\s*\*\s?/u, '')}`,
41
+ ...lines.slice(2, -2).map(indenter),
42
+ `${indenter(lines.at(-2))} */`
43
+ ].join('\n'));
44
+ }
45
+ });
46
+ }
47
+ }
48
+ })
49
+ },
50
+ // 一条语句作为 body 时强制换行缩进
51
+ // if (1) body
52
+ // 更改为
53
+ // if (1)
54
+ // body
55
+ 'nonblock-statement-body-position-with-indentation': {
56
+ meta,
57
+ create(context) {
58
+ const source = context.sourceCode;
59
+ const lines = source.getLines();
60
+ function lint(body, node) {
61
+ if (body.type !== 'BlockStatement') {
62
+ const token_before = source.getTokenBefore(body);
63
+ let indent = 0;
64
+ const line = lines[node.loc.start.line - 1];
65
+ for (let i = 0; i < line.length; i++)
66
+ if (line[i] === ' ')
67
+ indent++;
68
+ else if (line[i] === '\t')
69
+ indent += 4;
70
+ else
71
+ break;
72
+ if (token_before.loc.end.line === body.loc.start.line ||
73
+ body.loc.start.column !== indent + 4)
74
+ context.report({
75
+ node: body,
76
+ message: 'Expected a linebreak before this statement.',
77
+ fix: fixer => fixer.replaceTextRange([token_before.range[1], body.range[0]], '\n' + ' '.repeat(indent + 4))
78
+ });
79
+ }
80
+ }
81
+ return {
82
+ IfStatement(node) {
83
+ lint(node.consequent, node);
84
+ // Check the `else` node, but don't check 'else if' statements.
85
+ if (node.alternate && node.alternate.type !== 'IfStatement')
86
+ lint(node.alternate, node);
87
+ },
88
+ WhileStatement: node => lint(node.body, node),
89
+ DoWhileStatement: node => lint(node.body, node),
90
+ ForStatement: node => lint(node.body, node),
91
+ ForInStatement: node => lint(node.body, node),
92
+ ForOfStatement: node => lint(node.body, node)
93
+ };
94
+ },
95
+ },
96
+ // let o = { }
97
+ // let a = [ ]
98
+ // function foo () { }
99
+ 'empty-bracket-spacing': {
100
+ meta,
101
+ create(context) {
102
+ const source = context.sourceCode;
103
+ return {
104
+ ObjectExpression(node) {
105
+ // 是 {} 这样的空对象
106
+ if (node.properties.length === 0 && source.getText(node) === '{}')
107
+ context.report({
108
+ node,
109
+ message: 'Expected a space in this empty object.',
110
+ fix: fixer => fixer.replaceText(node, '{ }')
111
+ });
112
+ },
113
+ ArrayExpression(node) {
114
+ // 是 [] 这样的空数组
115
+ if (node.elements.length === 0 && source.getText(node) === '[]')
116
+ context.report({
117
+ node,
118
+ message: 'Expected a space in this empty array.',
119
+ fix: fixer => fixer.replaceText(node, '[ ]')
120
+ });
121
+ },
122
+ BlockStatement(node) {
123
+ if (node.body.length === 0 && source.getText(node) === '{}')
124
+ context.report({
125
+ node,
126
+ message: 'Expected a space in this empty block.',
127
+ fix: fixer => fixer.replaceText(node, '{ }')
128
+ });
129
+ },
130
+ };
131
+ }
132
+ },
133
+ // a + b**c
134
+ 'space-infix-ops-except-exponentiation': {
135
+ meta: {
136
+ ...meta,
137
+ messages: {
138
+ missingSpace: "Operator '{{operator}}' must be spaced.",
139
+ redundantSpace: "Operator '{{operator}}' must not be spaced."
140
+ }
141
+ },
142
+ // @ts-ignore
143
+ create(context) {
144
+ const int32Hint = true;
145
+ const { sourceCode } = context;
146
+ /** Returns the first token which violates the rule
147
+ @param {ASTNode} left The left node of the main node
148
+ @param {ASTNode} right The right node of the main node
149
+ @param {string} op The operator of the main node
150
+ @returns {Object} The violator token or null
151
+ @private */
152
+ function getFirstNonSpacedToken(left, right, op) {
153
+ const operator = sourceCode.getFirstTokenBetween(left, right, token => token.value === op);
154
+ const prev = sourceCode.getTokenBefore(operator);
155
+ const next = sourceCode.getTokenAfter(operator);
156
+ if (!sourceCode.isSpaceBetween(prev, operator) || !sourceCode.isSpaceBetween(operator, next))
157
+ return operator;
158
+ return null;
159
+ }
160
+ /**
161
+ Reports an AST node as a rule violation
162
+ @param {ASTNode} mainNode The node to report
163
+ @param {Object} culpritToken The token which has a problem
164
+ @returns {void}
165
+ @private
166
+ */
167
+ function report(mainNode, culpritToken) {
168
+ context.report({
169
+ node: mainNode,
170
+ loc: culpritToken.loc,
171
+ messageId: 'missingSpace',
172
+ data: {
173
+ operator: culpritToken.value
174
+ },
175
+ fix(fixer) {
176
+ const previousToken = sourceCode.getTokenBefore(culpritToken);
177
+ const afterToken = sourceCode.getTokenAfter(culpritToken);
178
+ let fixString = '';
179
+ if (culpritToken.range[0] - previousToken.range[1] === 0)
180
+ fixString = ' ';
181
+ fixString += culpritToken.value;
182
+ if (afterToken.range[0] - culpritToken.range[1] === 0)
183
+ fixString += ' ';
184
+ return fixer.replaceText(culpritToken, fixString);
185
+ }
186
+ });
187
+ }
188
+ /**
189
+ Check if the node is binary then report
190
+ @param {ASTNode} node node to evaluate
191
+ @returns {void}
192
+ @private
193
+ */
194
+ function checkBinary(node) {
195
+ const leftNode = node.left.typeAnnotation ? node.left.typeAnnotation : node.left;
196
+ const rightNode = node.right;
197
+ // search for = in AssignmentPattern nodes
198
+ const operator = node.operator || '=';
199
+ // 找到 ** operator
200
+ if (operator === '**') {
201
+ const op = sourceCode.getFirstTokenBetween(leftNode, rightNode, token => token.value === operator);
202
+ const prev = sourceCode.getTokenBefore(op);
203
+ const next = sourceCode.getTokenAfter(op);
204
+ if (sourceCode.isSpaceBetween(prev, op) || sourceCode.isSpaceBetween(op, next))
205
+ context.report({
206
+ node,
207
+ loc: op.loc,
208
+ messageId: 'redundantSpace',
209
+ data: {
210
+ operator: op.value
211
+ },
212
+ fix(fixer) {
213
+ const previousToken = sourceCode.getTokenBefore(op);
214
+ const afterToken = sourceCode.getTokenAfter(op);
215
+ let fixes = [];
216
+ if (op.range[0] > previousToken.range[1])
217
+ fixes.push(
218
+ // @ts-ignore
219
+ fixer.removeRange([previousToken.range[1], op.range[0]]));
220
+ if (afterToken.range[0] > op.range[1])
221
+ fixes.push(
222
+ // @ts-ignore
223
+ fixer.removeRange([op.range[1], afterToken.range[0]]));
224
+ return fixes;
225
+ }
226
+ });
227
+ }
228
+ else {
229
+ const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode, operator);
230
+ if (nonSpacedNode)
231
+ if (!(int32Hint && sourceCode.getText(node).endsWith('|0')))
232
+ report(node, nonSpacedNode);
233
+ }
234
+ }
235
+ /**
236
+ Check if the node is conditional
237
+ @param {ASTNode} node node to evaluate
238
+ @returns {void}
239
+ @private
240
+ */
241
+ function checkConditional(node) {
242
+ const nonSpacedConsequentNode = getFirstNonSpacedToken(node.test, node.consequent, '?');
243
+ const nonSpacedAlternateNode = getFirstNonSpacedToken(node.consequent, node.alternate, ':');
244
+ if (nonSpacedConsequentNode)
245
+ report(node, nonSpacedConsequentNode);
246
+ if (nonSpacedAlternateNode)
247
+ report(node, nonSpacedAlternateNode);
248
+ }
249
+ /**
250
+ Check if the node is a variable
251
+ @param {ASTNode} node node to evaluate
252
+ @returns {void}
253
+ @private
254
+ */
255
+ function checkVar(node) {
256
+ const leftNode = node.id.typeAnnotation ? node.id.typeAnnotation : node.id;
257
+ const rightNode = node.init;
258
+ if (rightNode) {
259
+ const nonSpacedNode = getFirstNonSpacedToken(leftNode, rightNode, '=');
260
+ if (nonSpacedNode)
261
+ report(node, nonSpacedNode);
262
+ }
263
+ }
264
+ function ts_report(operator) {
265
+ context.report({
266
+ node: operator,
267
+ messageId: 'missingSpace',
268
+ data: {
269
+ operator: operator.value
270
+ },
271
+ fix(fixer) {
272
+ const previousToken = sourceCode.getTokenBefore(operator);
273
+ const afterToken = sourceCode.getTokenAfter(operator);
274
+ let fixString = '';
275
+ if (operator.range[0] - previousToken.range[1] === 0)
276
+ fixString = ' ';
277
+ fixString += operator.value;
278
+ if (afterToken.range[0] - operator.range[1] === 0)
279
+ fixString += ' ';
280
+ return fixer.replaceText(operator, fixString);
281
+ }
282
+ });
283
+ }
284
+ function isSpaceChar(token) {
285
+ return token.type === AST_TOKEN_TYPES.Punctuator && /^[=?:]$/.test(token.value);
286
+ }
287
+ function checkAndReportAssignmentSpace(leftNode, rightNode) {
288
+ if (!rightNode || !leftNode)
289
+ return;
290
+ const operator = sourceCode.getFirstTokenBetween(leftNode, rightNode, isSpaceChar);
291
+ const prev = sourceCode.getTokenBefore(operator);
292
+ const next = sourceCode.getTokenAfter(operator);
293
+ if (!sourceCode.isSpaceBetween(prev, operator) || !sourceCode.isSpaceBetween(operator, next))
294
+ ts_report(operator);
295
+ }
296
+ /**
297
+ Check if it has an assignment char and report if it's faulty
298
+ @param node The node to report
299
+ */
300
+ function checkForEnumAssignmentSpace(node) {
301
+ checkAndReportAssignmentSpace(node.id, node.initializer);
302
+ }
303
+ /**
304
+ Check if it has an assignment char and report if it's faulty
305
+ @param node The node to report
306
+ */
307
+ function checkForPropertyDefinitionAssignmentSpace(node) {
308
+ const leftNode = node.optional && !node.typeAnnotation ? sourceCode.getTokenAfter(node.key) : node.typeAnnotation ?? node.key;
309
+ checkAndReportAssignmentSpace(leftNode, node.value);
310
+ }
311
+ /**
312
+ Check if it is missing spaces between type annotations chaining
313
+ @param typeAnnotation TypeAnnotations list
314
+ */
315
+ function checkForTypeAnnotationSpace(typeAnnotation) {
316
+ const types = typeAnnotation.types;
317
+ types.forEach(type => {
318
+ const skipFunctionParenthesis = type.type === TSESTree.AST_NODE_TYPES.TSFunctionType ? ASTUtils.isNotOpeningBraceToken : 0;
319
+ const operator = sourceCode.getTokenBefore(type, skipFunctionParenthesis);
320
+ if (operator && UNIONS.includes(operator.value)) {
321
+ const prev = sourceCode.getTokenBefore(operator);
322
+ const next = sourceCode.getTokenAfter(operator);
323
+ if (!sourceCode.isSpaceBetween(prev, operator) || !sourceCode.isSpaceBetween(operator, next))
324
+ ts_report(operator);
325
+ }
326
+ });
327
+ }
328
+ /**
329
+ Check if it has an assignment char and report if it's faulty
330
+ @param node The node to report
331
+ */
332
+ function checkForTypeAliasAssignment(node) {
333
+ checkAndReportAssignmentSpace(node.typeParameters ?? node.id, node.typeAnnotation);
334
+ }
335
+ function checkForTypeConditional(node) {
336
+ checkAndReportAssignmentSpace(node.extendsType, node.trueType);
337
+ checkAndReportAssignmentSpace(node.trueType, node.falseType);
338
+ }
339
+ return {
340
+ AssignmentExpression: checkBinary,
341
+ AssignmentPattern: checkBinary,
342
+ BinaryExpression: checkBinary,
343
+ LogicalExpression: checkBinary,
344
+ ConditionalExpression: checkConditional,
345
+ VariableDeclarator: checkVar,
346
+ TSEnumMember: checkForEnumAssignmentSpace,
347
+ PropertyDefinition: checkForPropertyDefinitionAssignmentSpace,
348
+ TSTypeAliasDeclaration: checkForTypeAliasAssignment,
349
+ TSUnionType: checkForTypeAnnotationSpace,
350
+ TSIntersectionType: checkForTypeAnnotationSpace,
351
+ TSConditionalType: checkForTypeConditional
352
+ };
353
+ }
354
+ },
355
+ // for (;;)
356
+ // 1;
357
+ // for (let i = 0; i < 10; i++)
358
+ // for ( ; i < 10; )
359
+ 'space-in-for-statement': {
360
+ meta,
361
+ create(context) {
362
+ const source = context.sourceCode;
363
+ return {
364
+ ForStatement(node) {
365
+ const paren_left = source.getFirstToken(node, token => token.value === '(');
366
+ const semi0 = source.getTokenAfter(node.init || paren_left);
367
+ const semi1 = source.getTokenAfter(node.test || semi0);
368
+ const semi0_prev = source.getTokenBefore(semi0, { includeComments: true });
369
+ const semi0_next = source.getTokenAfter(semi0, { includeComments: true });
370
+ const semi1_next = source.getTokenAfter(semi1, { includeComments: true });
371
+ if (node.init || node.test || node.update) {
372
+ // for (let i = 0; i < 10; i++)
373
+ // for ( ; i < 10; )
374
+ // for ( ; ; i++)
375
+ // for (let i = 0;)
376
+ if (node.init) {
377
+ if (semi0.loc.start.line === semi0_prev.loc.end.line &&
378
+ semi0.loc.start.column !== semi0_prev.loc.end.column)
379
+ context.report({
380
+ loc: {
381
+ start: semi0_prev.loc.end,
382
+ end: semi0.loc.start
383
+ },
384
+ message: 'for 语句第一个分号前面应该没有多余空格',
385
+ node,
386
+ fix: fixer => fixer.removeRange([semi0_prev.range[1], semi0.range[0]])
387
+ });
388
+ }
389
+ else // for ( ;)
390
+ if (semi0.loc.start.line === semi0_prev.loc.end.line &&
391
+ semi0.loc.start.column - semi0_prev.loc.end.column !== 2)
392
+ context.report({
393
+ loc: {
394
+ start: semi0_prev.loc.end,
395
+ end: semi0.loc.start
396
+ },
397
+ message: 'for 语句第一个分号前面应该为 2 个空格',
398
+ node,
399
+ fix: fixer => fixer.replaceTextRange([semi0_prev.range[1], semi0.range[0]], ' ')
400
+ });
401
+ // for ( ; ;)
402
+ if (semi0_next.loc.start.line === semi0.loc.end.line &&
403
+ semi0_next.loc.start.column - semi0.loc.end.column !== 2)
404
+ context.report({
405
+ loc: {
406
+ start: semi0.loc.end,
407
+ end: semi0_next.loc.start
408
+ },
409
+ message: 'for 语句第一个分号后面应为 2 个空格',
410
+ node,
411
+ fix: fixer => fixer.replaceTextRange([semi0.range[1], semi0_next.range[0]], ' ')
412
+ });
413
+ // // for ( ; i > 0;)
414
+ if (node.test) {
415
+ const test_end = source.getLastToken(node.test);
416
+ if (semi1.loc.start.line === test_end.loc.end.line &&
417
+ semi1.loc.start.column !== test_end.loc.end.column)
418
+ context.report({
419
+ loc: {
420
+ start: test_end.loc.end,
421
+ end: semi1.loc.start,
422
+ },
423
+ message: 'for 语句第二个分号前面没有多余的空格',
424
+ node,
425
+ fix: fixer => fixer.removeRange([test_end.range[1], semi1.range[0]])
426
+ });
427
+ }
428
+ if (semi1_next.loc.start.line === semi1.loc.end.line &&
429
+ semi1_next.loc.start.column - semi1.loc.end.column !== 2)
430
+ context.report({
431
+ loc: {
432
+ start: semi1.loc.end,
433
+ end: semi1_next.loc.start
434
+ },
435
+ message: 'for 语句第二个分号后面应为 2 个空格',
436
+ node,
437
+ fix: fixer => fixer.replaceTextRange([semi1.range[1], semi1_next.range[0]], ' ')
438
+ });
439
+ }
440
+ else { // for (;;)
441
+ if (semi0.loc.start.line === semi0_prev.loc.end.line &&
442
+ semi0.loc.start.column !== semi0_prev.loc.end.column)
443
+ context.report({
444
+ loc: {
445
+ start: semi0_prev.loc.end,
446
+ end: semi0.loc.start
447
+ },
448
+ message: '无条件 for 语句第一个分号前面应该没有空格',
449
+ node,
450
+ fix: fixer => fixer.removeRange([semi0_prev.range[1], semi0.range[0]])
451
+ });
452
+ if (semi0_next.loc.start.line === semi0.loc.end.line &&
453
+ semi0_next.loc.start.column !== semi0.loc.end.column)
454
+ context.report({
455
+ loc: {
456
+ start: semi0.loc.end,
457
+ end: semi0_next.loc.start
458
+ },
459
+ message: '无条件 for 语句第一个分号后面应该没有空格',
460
+ node,
461
+ fix: fixer => fixer.removeRange([semi0.range[1], semi0_next.range[0]])
462
+ });
463
+ if (semi1_next.loc.start.line === semi1.loc.end.line &&
464
+ semi1_next.loc.start.column !== semi1.loc.end.column)
465
+ context.report({
466
+ loc: {
467
+ start: semi1.loc.end,
468
+ end: semi1_next.loc.start
469
+ },
470
+ message: '无条件 for 语句第二个分号后面应该没有空格',
471
+ node,
472
+ fix: fixer => fixer.removeRange([semi1.range[1], semi1_next.range[0]])
473
+ });
474
+ }
475
+ },
476
+ };
477
+ }
478
+ },
479
+ // return (
480
+ // <div>1234</div>
481
+ // )
482
+ // 改为
483
+ // return <div>1234</div>
484
+ 'jsx-no-redundant-parenthesis-in-return': {
485
+ meta,
486
+ create(context) {
487
+ const source = context.sourceCode;
488
+ return {
489
+ ReturnStatement(node) {
490
+ if (node.argument?.type === 'JSXElement' || node.argument?.type === 'JSXFragment') {
491
+ const paren0 = source.getTokenBefore(node.argument);
492
+ const paren1 = source.getTokenAfter(node.argument);
493
+ if (paren0.type === 'Punctuator' && paren0.value === '(' &&
494
+ paren1.type === 'Punctuator' && paren1.value === ')') {
495
+ const paren0_next = source.getTokenAfter(paren0);
496
+ const paren1_prev = source.getTokenBefore(paren1);
497
+ context.report({
498
+ loc: {
499
+ start: paren0.loc.start,
500
+ end: paren1.loc.end
501
+ },
502
+ message: 'return jsx 表达式不应该有多余的括号',
503
+ node,
504
+ fix: fixer => ([
505
+ fixer.removeRange([paren0.range[0], paren0_next.range[0]]),
506
+ fixer.removeRange([paren1_prev.range[1], paren1.range[1]])
507
+ ])
508
+ });
509
+ }
510
+ }
511
+ },
512
+ ArrowFunctionExpression(node) {
513
+ if (node.body?.type === 'JSXElement' || node.body?.type === 'JSXFragment') {
514
+ const paren0 = source.getTokenBefore(node.body);
515
+ const paren1 = source.getTokenAfter(node.body);
516
+ if (paren0.type === 'Punctuator' && paren0.value === '(' &&
517
+ paren1.type === 'Punctuator' && paren1.value === ')') {
518
+ const paren0_next = source.getTokenAfter(paren0);
519
+ const paren1_prev = source.getTokenBefore(paren1);
520
+ context.report({
521
+ loc: {
522
+ start: paren0.loc.start,
523
+ end: paren1.loc.end
524
+ },
525
+ message: 'return jsx 表达式不应该有多余的括号',
526
+ node,
527
+ fix: fixer => ([
528
+ fixer.removeRange([paren0.range[0], paren0_next.range[0]]),
529
+ fixer.removeRange([paren1_prev.range[1], paren1.range[1]])
530
+ ])
531
+ });
532
+ }
533
+ }
534
+ },
535
+ };
536
+ }
537
+ },
538
+ 'keep-indent': {
539
+ meta,
540
+ create(context) {
541
+ const source = context.sourceCode;
542
+ const lines = source.getLines();
543
+ return {
544
+ Program() {
545
+ // 参考 keep_indent 逻辑
546
+ if (lines.length >= 2) {
547
+ let last_line = { indent: 0, text: '' };
548
+ for (let i = 0; i < lines.length - 1; i++) {
549
+ const _line = split_indent(lines[i]);
550
+ if (!_line.indent && !_line.text && last_line.indent) {
551
+ const pos = { column: 0, line: i + 1 };
552
+ const index = source.getIndexFromLoc(pos);
553
+ const node = source.getNodeByRangeIndex(index);
554
+ if (node && node.type !== 'Program')
555
+ context.report({
556
+ message: '空行应保留与上一个区块相同的缩进',
557
+ loc: { start: pos, end: pos },
558
+ fix: fixer => fixer.replaceTextRange([index, index], ' '.repeat(last_line.indent))
559
+ });
560
+ }
561
+ last_line = _line;
562
+ }
563
+ }
564
+ }
565
+ };
566
+ }
567
+ },
568
+ 'func-style': {
569
+ meta,
570
+ create(context) {
571
+ const source = context.sourceCode;
572
+ let stack = [];
573
+ const fix = (fixer, node) => fixer.replaceText(node.parent.parent, `${node.async ? 'async ' : ''}function ${node.generator ? '* ' : ''}` +
574
+ // todo: 丢失了泛型参数 const foo = <D = Zippable> (xxx: D) => { },如何加回来?
575
+ node.parent.id.name +
576
+ ' (' +
577
+ node.params.map(param => source.getText(param)).join(', ') +
578
+ ') ' +
579
+ source.getText(node.body));
580
+ return {
581
+ FunctionDeclaration(node) {
582
+ stack.push(false);
583
+ },
584
+ 'FunctionDeclaration:exit'() {
585
+ stack.pop();
586
+ },
587
+ FunctionExpression(node) {
588
+ stack.push(false);
589
+ if (node.parent.type === 'VariableDeclarator')
590
+ context.report({
591
+ node: node.parent.parent,
592
+ message: '函数应该使用 function foo { } 这样来声明而不是 const foo = function () { }',
593
+ loc: node.parent.parent.loc,
594
+ fix: fixer => fix(fixer, node)
595
+ });
596
+ },
597
+ 'FunctionExpression:exit'() {
598
+ stack.pop();
599
+ },
600
+ ThisExpression() {
601
+ if (stack.length > 0)
602
+ stack[stack.length - 1] = true;
603
+ },
604
+ ArrowFunctionExpression() {
605
+ stack.push(false);
606
+ },
607
+ 'ArrowFunctionExpression:exit'(node) {
608
+ if (!stack.pop() && // not has this expr
609
+ node.parent.type === 'VariableDeclarator' &&
610
+ node.body.type === 'BlockStatement' && node.body.body.length // 多条语句才转换
611
+ )
612
+ context.report({
613
+ node: node.parent.parent,
614
+ message: '多条语句的函数应该使用 function foo { } 这样来声明而不是 const foo = () => { }',
615
+ loc: node.parent.parent.loc,
616
+ fix: fixer => fix(fixer, node)
617
+ });
618
+ }
619
+ };
620
+ }
621
+ },
622
+ // 规则太复杂了,暂时写不出来
623
+ // ... color ? [ ] : [
624
+ // '-c', 'color.status=false',
625
+ // '-c', 'color.ui=false',
626
+ // '-c', 'color.branch=false',
627
+ // '-c', 'color.ui=false',
628
+ // ],
629
+ // test ?
630
+ // ...
631
+ // : [ ]
632
+ // 'conditional-expresssion-style': {
633
+ // meta,
634
+ // create (context) {
635
+ // const source = context.sourceCode
636
+ // const lines = source.getLines()
637
+ // return {
638
+ // ConditionalExpression (node) {
639
+ // const question = source.getTokenAfter(node.test, not_closing_paren)
640
+ // const colon = source.getTokenAfter(node.consequent, not_closing_paren)
641
+ // const test_tail = source.getTokenBefore(question, { includeComments: true })
642
+ // const test_head = source.getFirstToken(node, { includeComments: true })
643
+ // const consequent_start = source.getTokenAfter(question, not_opening_paren)
644
+ // const consequent_tail = source.getTokenBefore(colon, not_closing_paren)
645
+ // const alternate_start = source.getTokenAfter(colon, not_opening_paren)
646
+ // const node_tail = source.getLastToken(node)
647
+ // // 三元运算符的 ? 应该和 test 条件末尾在同一行
648
+ // if (test_tail.loc.start.line !== question.loc.start.line)
649
+ // context.report({
650
+ // node,
651
+ // loc: {
652
+ // start: colon.loc.start,
653
+ // end: colon.loc.end
654
+ // },
655
+ // message: '条件表达式的 ? 应该和 test 条件末尾在同一行',
656
+ // fix: fixer => fixer.replaceTextRange([test_tail.range[1], colon.range[0]], ' ')
657
+ // })
658
+ // // 单行的条件表达式
659
+ // if (colon.loc.start.line === question.loc.start.line) {
660
+ // const alternate_start_with_comments = source.getTokenAfter(colon, { includeComments: true, filter: not_opening_paren })
661
+ // if (node_tail.loc.end.line !== colon.loc.start.line)
662
+ // context.report({
663
+ // node,
664
+ // loc: {
665
+ // start: node.alternate.loc.start,
666
+ // end: node_tail.loc.end
667
+ // },
668
+ // message: '单行条件表达式的 else 条件也应该在同一行',
669
+ // fix: fixer => fixer.replaceTextRange([colon.range[1], alternate_start_with_comments.range[0]], ' ')
670
+ // })
671
+ // } else { // 多行条件表达式
672
+ // if (consequent_tail.loc.end.line === colon.loc.start.line)
673
+ // context.report({
674
+ // node,
675
+ // loc: {
676
+ // start: consequent_tail.loc.start,
677
+ // end: colon.loc.start
678
+ // },
679
+ // message: '多行条件表达式的 : 应该在单独的一行',
680
+ // fix: fixer =>
681
+ // fixer.insertTextBefore(
682
+ // colon,
683
+ // '\n' +
684
+ // ' '.repeat(
685
+ // Math.max(split_indent(lines[consequent_tail.loc.end.line - 1]).indent - 4, 0)
686
+ // )
687
+ // )
688
+ // })
689
+ // if (
690
+ // alternate_start.loc.start.line === colon.loc.start.line &&
691
+ // (node.alternate.loc.start.line !== node.alternate.loc.end.line)
692
+ // )
693
+ // context.report({
694
+ // node,
695
+ // loc: {
696
+ // start: colon.loc.end,
697
+ // end: alternate_start.loc.start
698
+ // },
699
+ // message: '多行条件表达式的 : 应该在单独的一行',
700
+ // fix: fixer => fixer.insertTextBefore(
701
+ // alternate_start,
702
+ // '\n' +
703
+ // ' '.repeat(
704
+ // split_indent(lines[colon.loc.start.line - 1]).indent + 4
705
+ // )
706
+ // )
707
+ // })
708
+ // if (consequent_start.loc.start.line === question.loc.start.line)
709
+ // context.report({
710
+ // node,
711
+ // loc: {
712
+ // start: question.loc.end,
713
+ // end: test_head.loc.start
714
+ // },
715
+ // message: '多行条件表达式的 ? 这一行后面不应该有其他表达式,请换行',
716
+ // fix: fixer => fixer.insertTextBefore(
717
+ // consequent_start,
718
+ // '\n' +
719
+ // ' '.repeat(
720
+ // split_indent(lines[colon.loc.start.line - 1]).indent + 4
721
+ // )
722
+ // )
723
+ // })
724
+ // }
725
+ // }
726
+ // }
727
+ // }
728
+ // }
729
+ }
730
+ };
731
+ export const xlint_config = {
732
+ files: ['**/*.{js,mjs,cjs,ts,tsx,mts,cts}'],
733
+ ignores: [
734
+ '*.d.ts',
735
+ 'node_modules/',
736
+ '.git/',
737
+ ],
738
+ languageOptions: {
739
+ ecmaVersion: 'latest',
740
+ parser: TSParser,
741
+ parserOptions: {
742
+ ecmaVersion: 'latest',
743
+ sourceType: 'module',
744
+ project: './tsconfig.json',
745
+ ecmaFeatures: {
746
+ jsx: true
747
+ },
748
+ },
749
+ },
750
+ plugins: {
751
+ '@typescript-eslint': ts_plugin,
752
+ react: react_plugin,
753
+ import: import_plugin,
754
+ xlint: xlint_plugin,
755
+ },
756
+ settings: {
757
+ react: {
758
+ version: 'detect'
759
+ }
760
+ },
761
+ rules: {
762
+ 'xlint/fold-jsdoc-comments': 'error',
763
+ // 取代 nonblock-statement-body-position
764
+ 'xlint/nonblock-statement-body-position-with-indentation': 'error',
765
+ 'xlint/empty-bracket-spacing': 'error',
766
+ // a + b**c
767
+ 'xlint/space-infix-ops-except-exponentiation': 'error',
768
+ 'xlint/space-in-for-statement': 'error',
769
+ 'xlint/jsx-no-redundant-parenthesis-in-return': 'error',
770
+ 'xlint/keep-indent': 'error',
771
+ // 函数使用 function 来声明而不是 const foo = () => { }
772
+ 'xlint/func-style': 'error',
773
+ '@typescript-eslint/semi': ['error', 'never'],
774
+ '@typescript-eslint/no-extra-semi': 'error',
775
+ 'semi-style': ['error', 'first'],
776
+ // 使用 ===
777
+ eqeqeq: 'error',
778
+ // 父类尽量返回 this 类型
779
+ '@typescript-eslint/prefer-return-this-type': 'error',
780
+ // 尽量使用 . 访问属性而不是 []
781
+ '@typescript-eslint/dot-notation': 'error',
782
+ // 必须 throw Error
783
+ '@typescript-eslint/no-throw-literal': 'error',
784
+ // ------------ async
785
+ // 返回 Promise 的函数一定要标记为 async 函数
786
+ '@typescript-eslint/promise-function-async': 'error',
787
+ // 不要 return await promise, 直接 return promise, 除非外面有 try catch
788
+ '@typescript-eslint/return-await': 'error',
789
+ // ------------ 括号
790
+ // a => { } 而不是 (a) => { }
791
+ 'arrow-parens': ['error', 'as-needed', { requireForBlockBody: false }],
792
+ // 不要多余的大括号
793
+ // if (true)
794
+ // console.log()
795
+ curly: ['error', 'multi'],
796
+ // 简单属性不要冗余的大括号 <Component prop='simple-value' />
797
+ 'react/jsx-curly-brace-presence': ['error', 'never'],
798
+ // ------------ 空格
799
+ // { a, b } 这样的对象,大括号里面要有空格
800
+ '@typescript-eslint/object-curly-spacing': ['error', 'always'],
801
+ // [a, b, c]
802
+ '@typescript-eslint/comma-spacing': 'error',
803
+ // foo()
804
+ '@typescript-eslint/func-call-spacing': 'error',
805
+ // a => { } 中箭头左右两边空格
806
+ 'arrow-spacing': ['error'],
807
+ // 注释双斜杠后面要有空格
808
+ 'spaced-comment': ['error', 'always', { markers: ['/'] }],
809
+ // 函数声明中,名称后面要有空格
810
+ '@typescript-eslint/space-before-function-paren': 'error',
811
+ // { return true } 这样的 block 大括号里面要有空格
812
+ 'block-spacing': ['error', 'always'],
813
+ // aaa: 123
814
+ '@typescript-eslint/key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'minimum' }],
815
+ // aaa: string
816
+ '@typescript-eslint/type-annotation-spacing': 'error',
817
+ // if ()
818
+ '@typescript-eslint/keyword-spacing': ['error', { before: true, after: true }],
819
+ // if (1) { }
820
+ '@typescript-eslint/space-before-blocks': 'error',
821
+ // case 1: ...
822
+ 'switch-colon-spacing': 'error',
823
+ // <Hello name={firstname} />
824
+ 'react/jsx-equals-spacing': ['error', 'never'],
825
+ // 不允许使用 tab
826
+ 'no-tabs': 'error',
827
+ // 使用 \n 换行
828
+ 'linebreak-style': ['error', 'unix'],
829
+ // 文件以 \n 结尾
830
+ 'eol-last': ['error', 'always'],
831
+ // ------------ 引号
832
+ // 用单引号
833
+ 'jsx-quotes': ['error', 'prefer-single'],
834
+ // 用单引号
835
+ quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
836
+ // 不要冗余的引号包裹属性
837
+ 'quote-props': ['error', 'as-needed', { keywords: false, unnecessary: true }],
838
+ // ------------ 其它
839
+ // boolean 属性不要冗余的 ={true} <Component boolprop />
840
+ 'react/jsx-boolean-value': ['error', 'never'],
841
+ // 没有 children 的 Component 写成闭合标签 <Component />
842
+ 'react/self-closing-comp': 'error',
843
+ // 单行类型声明用 `,` 分割,多行类型声明结尾不要加分号
844
+ '@typescript-eslint/member-delimiter-style': [
845
+ 'error',
846
+ {
847
+ multiline: {
848
+ delimiter: 'none',
849
+ requireLast: false
850
+ },
851
+ singleline: {
852
+ delimiter: 'comma',
853
+ requireLast: false
854
+ }
855
+ }
856
+ ],
857
+ '@typescript-eslint/prefer-includes': 'error',
858
+ '@typescript-eslint/prefer-regexp-exec': 'error',
859
+ // 禁止使用 export default
860
+ 'no-restricted-exports': [
861
+ 'error',
862
+ { restrictDefaultExports: { direct: true, named: true, defaultFrom: true, namedFrom: true, namespaceFrom: true } }
863
+ ],
864
+ '@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports', disallowTypeAnnotations: false }],
865
+ // () => { 返回 void 的表达式 }
866
+ '@typescript-eslint/no-confusing-void-expression': 'error',
867
+ // ------------ import
868
+ 'import/no-duplicates': ['error', { 'prefer-inline': true }],
869
+ 'import/order': [
870
+ 'error',
871
+ {
872
+ 'newlines-between': 'always-and-inside-groups'
873
+ }
874
+ ]
875
+ }
876
+ };
877
+ function split_indent(line) {
878
+ let i = 0;
879
+ let indent = 0;
880
+ for (; i < line.length; i++)
881
+ if (line[i] === ' ')
882
+ indent++;
883
+ else if (line[i] === '\t')
884
+ indent += 4;
885
+ else
886
+ break;
887
+ return {
888
+ indent,
889
+ text: line.slice(i)
890
+ };
891
+ }
892
+ function not_opening_paren(token) {
893
+ return !(['(', '[', '{'].includes(token.value) && token.type === 'Punctuator');
894
+ }
895
+ function not_closing_paren(token) {
896
+ return !([')', ']', '}'].includes(token.value) && token.type === 'Punctuator');
897
+ }
898
+ //# sourceMappingURL=xlint.js.map