@trigen/oxlint-config 9.0.3 → 9.1.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@trigen/oxlint-config",
3
3
  "type": "module",
4
- "version": "9.0.3",
4
+ "version": "9.1.0",
5
5
  "description": "Trigen's Oxlint config.",
6
6
  "author": "dangreen",
7
7
  "license": "MIT",
@@ -161,11 +161,13 @@ function getOptions(context) {
161
161
  }
162
162
 
163
163
  function getImportItems(imports, options) {
164
- return imports.map((node, index) => ({
165
- index,
166
- node,
167
- rank: getRank(node, options)
168
- }))
164
+ return imports
165
+ .filter(node => node.specifiers.length > 0)
166
+ .map((node, index) => ({
167
+ index,
168
+ node,
169
+ rank: getRank(node, options)
170
+ }))
169
171
  }
170
172
 
171
173
  function compareImportItems(left, right) {
@@ -189,23 +191,36 @@ function getFirstUnorderedPair(items) {
189
191
  return null
190
192
  }
191
193
 
192
- function hasInvalidNewlines(items, options) {
194
+ function hasBlankLineBetween(sourceCode, left, right) {
195
+ return sourceCode.text
196
+ .slice(left.node.range[1], right.node.range[0])
197
+ .split(/\r?\n/)
198
+ .slice(1, -1)
199
+ .some(line => line.trim() === '')
200
+ }
201
+
202
+ function getInvalidNewlinePair(sourceCode, items, options) {
193
203
  if (options['newlines-between'] === 'ignore') {
194
- return false
204
+ return null
195
205
  }
196
206
 
197
- return items.some((item, index) => {
198
- if (index === 0) {
199
- return false
200
- }
201
-
207
+ for (let index = 1; index < items.length; index++) {
202
208
  const previousItem = items[index - 1]
203
- const hasEmptyLine = item.node.loc.start.line - previousItem.node.loc.end.line > 1
209
+ const item = items[index]
210
+ const hasBlankLine = hasBlankLineBetween(sourceCode, previousItem, item)
211
+ const invalid = options['newlines-between'] === 'never'
212
+ ? hasBlankLine
213
+ : !hasBlankLine
204
214
 
205
- return options['newlines-between'] === 'never'
206
- ? hasEmptyLine
207
- : !hasEmptyLine
208
- })
215
+ if (invalid) {
216
+ return [
217
+ previousItem,
218
+ item
219
+ ]
220
+ }
221
+ }
222
+
223
+ return null
209
224
  }
210
225
 
211
226
  function hasInnerComments(sourceCode, items) {
@@ -216,13 +231,20 @@ function hasInnerComments(sourceCode, items) {
216
231
  && comment.range[1] < lastNode.range[1])
217
232
  }
218
233
 
219
- function hasSideEffectImports(items) {
220
- return items.some(({ node }) => node.specifiers.length === 0)
234
+ function canFix(sourceCode, items) {
235
+ return !hasInnerComments(sourceCode, items)
221
236
  }
222
237
 
223
- function canFix(sourceCode, items) {
224
- return !hasSideEffectImports(items)
225
- && !hasInnerComments(sourceCode, items)
238
+ function hasSkippedImportsBetween(imports, items) {
239
+ const itemNodes = new Set(items.map(({ node }) => node))
240
+ const [
241
+ start,
242
+ end
243
+ ] = getImportBlockRange(items)
244
+
245
+ return imports.some(node => !itemNodes.has(node)
246
+ && node.range[0] > start
247
+ && node.range[1] < end)
226
248
  }
227
249
 
228
250
  function getFixedImportText(sourceCode, items, options) {
@@ -316,21 +338,30 @@ export default {
316
338
 
317
339
  imports.push(node)
318
340
  },
319
- 'Program:exit'(node) {
341
+ 'Program:exit'() {
320
342
  if (imports.length < 2) {
321
343
  return
322
344
  }
323
345
 
324
346
  const sourceCode = context.sourceCode
325
347
  const items = getImportItems(imports, options)
348
+
349
+ if (items.length < 2) {
350
+ return
351
+ }
352
+
353
+ const hasSkippedImports = hasSkippedImportsBetween(imports, items)
326
354
  const unorderedPair = getFirstUnorderedPair(items)
327
- const invalidNewlines = hasInvalidNewlines(items, options)
355
+ const invalidNewlinePair = hasSkippedImports
356
+ ? null
357
+ : getInvalidNewlinePair(sourceCode, items, options)
328
358
 
329
- if (!unorderedPair && !invalidNewlines) {
359
+ if (!unorderedPair && !invalidNewlinePair) {
330
360
  return
331
361
  }
332
362
 
333
363
  const fix = canFix(sourceCode, items)
364
+ && !hasSkippedImports
334
365
  ? fixer => fixer.replaceTextRange(
335
366
  getImportBlockRange(items),
336
367
  getFixedImportText(sourceCode, items, options)
@@ -339,13 +370,10 @@ export default {
339
370
  const [
340
371
  previousItem,
341
372
  item
342
- ] = unorderedPair ?? [
343
- items[0],
344
- items[1]
345
- ]
373
+ ] = unorderedPair ?? invalidNewlinePair
346
374
 
347
375
  context.report({
348
- node,
376
+ node: item.node,
349
377
  message: unorderedPair
350
378
  ? getMessage(previousItem.rank, item.rank)
351
379
  : 'Import declarations have invalid empty lines.',
@@ -3,6 +3,7 @@ import importOrderRule from './import-order.js'
3
3
  import memberOrderingRule from './member-ordering.js'
4
4
  import namedImportOrderRule from './named-import-order.js'
5
5
  import namingConventionRule from './naming-convention.js'
6
+ import typeImportStyleRule from './type-import-style.js'
6
7
 
7
8
  export default {
8
9
  meta: {
@@ -13,6 +14,7 @@ export default {
13
14
  'import-order': importOrderRule,
14
15
  'member-ordering': memberOrderingRule,
15
16
  'named-import-order': namedImportOrderRule,
16
- 'naming-convention': namingConventionRule
17
+ 'naming-convention': namingConventionRule,
18
+ 'type-import-style': typeImportStyleRule
17
19
  }
18
20
  }
@@ -39,7 +39,10 @@ function getTypeRank(node, specifier, options) {
39
39
  }
40
40
 
41
41
  function getPatternRank(name, patterns) {
42
- const rank = patterns.findIndex(pattern => new RegExp(pattern).test(name))
42
+ const normalizedName = name.replace(/^\$+|\$+$/g, '')
43
+ const rank = patterns.findIndex(pattern => new RegExp(pattern).test(
44
+ normalizedName
45
+ ))
43
46
 
44
47
  return rank === -1 ? Number.POSITIVE_INFINITY : rank
45
48
  }
@@ -0,0 +1,166 @@
1
+ function shouldConvert(node) {
2
+ return node.importKind !== 'type'
3
+ && !hasImportAttributes(node)
4
+ && node.specifiers.length > 0
5
+ && node.specifiers.every(specifier => specifier.type === 'ImportSpecifier'
6
+ && specifier.importKind === 'type')
7
+ }
8
+
9
+ function isNamedImport(node) {
10
+ return node.specifiers.length > 0
11
+ && node.specifiers.every(specifier => specifier.type === 'ImportSpecifier')
12
+ }
13
+
14
+ function isTypeImport(node) {
15
+ return node.importKind === 'type'
16
+ || (
17
+ isNamedImport(node)
18
+ && node.specifiers.every(specifier => specifier.importKind === 'type')
19
+ )
20
+ }
21
+
22
+ function hasImportAttributes(node) {
23
+ return (node.attributes?.length ?? 0) > 0
24
+ || (node.assertions?.length ?? 0) > 0
25
+ }
26
+
27
+ function hasCommentsBetween(sourceCode, left, right) {
28
+ return sourceCode.getAllComments().some(comment => comment.range[0] > left.range[1]
29
+ && comment.range[1] < right.range[0])
30
+ }
31
+
32
+ function getLocalName(specifier) {
33
+ return specifier.local?.name ?? null
34
+ }
35
+
36
+ function hasOverlappingLocalNames(left, right) {
37
+ const leftNames = new Set(left.specifiers.map(getLocalName))
38
+
39
+ return right.specifiers.some(specifier => leftNames.has(getLocalName(specifier)))
40
+ }
41
+
42
+ function canMerge(sourceCode, typeNode, valueNode) {
43
+ return typeNode.source.value === valueNode.source.value
44
+ && isTypeImport(typeNode)
45
+ && isNamedImport(typeNode)
46
+ && isNamedImport(valueNode)
47
+ && !isTypeImport(valueNode)
48
+ && !hasImportAttributes(typeNode)
49
+ && !hasImportAttributes(valueNode)
50
+ && !hasOverlappingLocalNames(typeNode, valueNode)
51
+ && !hasCommentsBetween(sourceCode, typeNode, valueNode)
52
+ }
53
+
54
+ function getMergePair(imports, sourceCode) {
55
+ for (let index = 1; index < imports.length; index++) {
56
+ const previousImport = imports[index - 1]
57
+ const importNode = imports[index]
58
+
59
+ if (canMerge(sourceCode, previousImport, importNode)) {
60
+ return [
61
+ previousImport,
62
+ importNode
63
+ ]
64
+ }
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ function getConvertNode(imports) {
71
+ return imports.find(shouldConvert)
72
+ }
73
+
74
+ function getTypeSpecifierText(specifier, sourceCode) {
75
+ const text = sourceCode.getText(specifier)
76
+
77
+ return text.startsWith('type ')
78
+ ? text
79
+ : `type ${text}`
80
+ }
81
+
82
+ function getMergedText(typeNode, valueNode, sourceCode) {
83
+ const typeSpecifiers = typeNode.specifiers.map(specifier => getTypeSpecifierText(
84
+ specifier,
85
+ sourceCode
86
+ ))
87
+ const valueSpecifiers = valueNode.specifiers.map(specifier => sourceCode.getText(
88
+ specifier
89
+ ))
90
+ const source = sourceCode.getText(valueNode.source)
91
+
92
+ return `import { ${[
93
+ ...typeSpecifiers,
94
+ ...valueSpecifiers
95
+ ].join(', ')} } from ${source}`
96
+ }
97
+
98
+ function getFixedText(node, sourceCode) {
99
+ return sourceCode.getText(node)
100
+ .replace(/^import\b/, 'import type')
101
+ .replace(/([,{]\s*)type\s+/g, '$1')
102
+ }
103
+
104
+ export default {
105
+ meta: {
106
+ type: 'layout',
107
+ fixable: 'code',
108
+ docs: {
109
+ description: 'Prefer import type and merge duplicate type/value imports.'
110
+ },
111
+ schema: []
112
+ },
113
+ create(context) {
114
+ const sourceCode = context.sourceCode
115
+ const imports = []
116
+
117
+ return {
118
+ ImportDeclaration(node) {
119
+ if (typeof node.source.value !== 'string') {
120
+ return
121
+ }
122
+
123
+ imports.push(node)
124
+ },
125
+ 'Program:exit'() {
126
+ const mergePair = getMergePair(imports, sourceCode)
127
+
128
+ if (mergePair) {
129
+ const [
130
+ typeNode,
131
+ valueNode
132
+ ] = mergePair
133
+
134
+ context.report({
135
+ node: valueNode.source,
136
+ message: 'Merge type and value imports from the same source.',
137
+ fix: fixer => fixer.replaceTextRange(
138
+ [
139
+ typeNode.range[0],
140
+ valueNode.range[1]
141
+ ],
142
+ getMergedText(typeNode, valueNode, sourceCode)
143
+ )
144
+ })
145
+
146
+ return
147
+ }
148
+
149
+ const convertNode = getConvertNode(imports)
150
+
151
+ if (!convertNode) {
152
+ return
153
+ }
154
+
155
+ context.report({
156
+ node: convertNode.source,
157
+ message: 'Use import type when all named imports are type imports.',
158
+ fix: fixer => fixer.replaceTextRange(
159
+ convertNode.range,
160
+ getFixedText(convertNode, sourceCode)
161
+ )
162
+ })
163
+ }
164
+ }
165
+ }
166
+ }
package/src/storybook.js CHANGED
@@ -8,6 +8,11 @@ export default {
8
8
  overrides: [
9
9
  {
10
10
  files: storiesFiles,
11
+ plugins: [
12
+ 'import',
13
+ 'react',
14
+ 'typescript'
15
+ ],
11
16
  rules: {
12
17
  'eslint/max-classes-per-file': 'off',
13
18
  'eslint/no-magic-numbers': 'off',
@@ -161,7 +161,12 @@ export default {
161
161
  'eslint/no-useless-call': 'error',
162
162
  'eslint/no-useless-concat': 'error',
163
163
  'eslint/no-useless-return': 'error',
164
- 'eslint/no-void': 'error',
164
+ 'eslint/no-void': [
165
+ 'error',
166
+ {
167
+ allowAsStatement: true
168
+ }
169
+ ],
165
170
  'eslint/prefer-promise-reject-errors': 'error',
166
171
  'eslint/prefer-regex-literals': 'error',
167
172
  'eslint/preserve-caught-error': 'error',
@@ -276,15 +281,18 @@ export default {
276
281
  },
277
282
  {
278
283
  selector: 'typeLike',
279
- format: ['PascalCase']
284
+ format: ['PascalCase'],
285
+ trailingDollar: 'allow'
280
286
  },
281
287
  {
282
288
  selector: 'interface',
283
- format: ['PascalCase']
289
+ format: ['PascalCase'],
290
+ trailingDollar: 'allow'
284
291
  },
285
292
  {
286
293
  selector: 'enumMember',
287
- format: ['PascalCase']
294
+ format: ['PascalCase'],
295
+ trailingDollar: 'allow'
288
296
  },
289
297
  {
290
298
  selector: 'classProperty',
@@ -8,6 +8,7 @@ export default {
8
8
  overrides: [
9
9
  {
10
10
  files: configFiles,
11
+ plugins: ['import'],
11
12
  rules: {
12
13
  'import/no-default-export': 'off',
13
14
  'import/no-anonymous-default-export': 'off'
@@ -32,6 +32,7 @@ export default {
32
32
  overrides: [
33
33
  {
34
34
  files: tsFiles,
35
+ plugins: ['jsdoc'],
35
36
  rules: {
36
37
  'jsdoc/require-param': 'off',
37
38
  'jsdoc/require-yields-type': 'off'
@@ -11,6 +11,7 @@ export default {
11
11
  overrides: [
12
12
  {
13
13
  files: jsxFiles,
14
+ plugins: ['jsdoc'],
14
15
  rules: {
15
16
  'jsdoc/require-param': 'off',
16
17
  'jsdoc/require-returns': 'off'
@@ -19,7 +19,14 @@ export default {
19
19
  'stylistic-js/jsx-child-element-spacing': 'error',
20
20
  'stylistic-js/jsx-closing-bracket-location': 'error',
21
21
  'stylistic-js/jsx-closing-tag-location': 'error',
22
- 'react/jsx-curly-brace-presence': ['error', 'never'],
22
+ 'react/jsx-curly-brace-presence': [
23
+ 'error',
24
+ {
25
+ children: 'never',
26
+ propElementValues: 'always',
27
+ props: 'never'
28
+ }
29
+ ],
23
30
  'stylistic-js/jsx-curly-newline': 'error',
24
31
  'stylistic-js/jsx-curly-spacing': 'error',
25
32
  'stylistic-js/jsx-equals-spacing': 'error',
@@ -53,7 +53,6 @@ export default {
53
53
  'typescript/no-unnecessary-qualifier': 'error',
54
54
  'typescript/no-unnecessary-template-expression': 'error',
55
55
  'typescript/no-unnecessary-type-arguments': 'error',
56
- 'typescript/consistent-type-imports': 'error',
57
56
  'typescript/prefer-includes': 'error',
58
57
  'typescript/prefer-nullish-coalescing': 'off',
59
58
  'typescript/prefer-optional-chain': 'error',
@@ -68,13 +68,21 @@ export default {
68
68
  fixMixedExportsWithInlineTypeSpecifier: true
69
69
  }
70
70
  ],
71
+ 'typescript/consistent-type-imports': 'error',
71
72
  'typescript/explicit-module-boundary-types': 'off',
72
73
  'typescript/no-dynamic-delete': 'error',
73
74
  'typescript/no-extraneous-class': 'error',
74
- 'typescript/no-invalid-void-type': 'error',
75
+ 'typescript/no-invalid-void-type': [
76
+ 'error',
77
+ {
78
+ allowAsThisParameter: true,
79
+ allowInGenericTypeArguments: true
80
+ }
81
+ ],
75
82
  'typescript/prefer-for-of': 'error',
76
83
  'typescript/prefer-function-type': 'error',
77
84
  'typescript/unified-signatures': 'error',
85
+ 'trigen/type-import-style': 'error',
78
86
  'trigen/member-ordering': [
79
87
  'error',
80
88
  {
@@ -108,6 +116,7 @@ export default {
108
116
  },
109
117
  {
110
118
  files: dtsFiles,
119
+ plugins: ['import'],
111
120
  rules: {
112
121
  'import/unambiguous': 'off'
113
122
  }
package/src/test.js CHANGED
@@ -11,6 +11,8 @@ export default {
11
11
  env: {
12
12
  vitest: true
13
13
  },
14
+ plugins: ['typescript'],
15
+ jsPlugins: ['@trigen/oxlint-config/plugin'],
14
16
  rules: {
15
17
  'eslint/max-classes-per-file': 'off',
16
18
  'eslint/no-magic-numbers': 'off',
@@ -25,7 +27,10 @@ export default {
25
27
  'trigen/import-order': 'off',
26
28
  'eslint/prefer-destructuring': 'off',
27
29
  'eslint/no-loop-func': 'off',
28
- 'typescript/no-misused-promises': 'off'
30
+ 'typescript/no-misused-promises': 'off',
31
+ 'eslint/no-use-before-define': 'off',
32
+ 'eslint/no-useless-assignment': 'off',
33
+ 'eslint/no-empty-function': 'off'
29
34
 
30
35
  // Unsupported by Oxlint
31
36
  // 'eslint/camelcase': 'off',