@trigen/oxlint-config 9.0.3 → 9.2.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.2.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
  }
@@ -128,6 +131,11 @@ function hasInnerComments(sourceCode, range) {
128
131
  && comment.range[1] < range[1])
129
132
  }
130
133
 
134
+ function shouldBeMultiline(sourceCode, range, items) {
135
+ return items.length > 1
136
+ && !sourceCode.text.slice(range[0], range[1]).includes('\n')
137
+ }
138
+
131
139
  function getFixedText(node, items, options, sourceCode, range) {
132
140
  const text = sourceCode.text
133
141
  const content = text.slice(range[0], range[1])
@@ -135,14 +143,16 @@ function getFixedText(node, items, options, sourceCode, range) {
135
143
  .sort(compareItems)
136
144
  .map(({ specifier }) => sourceCode.getText(specifier))
137
145
 
138
- if (!content.includes('\n')) {
146
+ if (!content.includes('\n') && items.length < 2) {
139
147
  return ` ${sortedSpecifiers.join(', ')} `
140
148
  }
141
149
 
142
150
  const linebreak = getLinebreak(text)
143
151
  const firstSpecifier = items[0].specifier
144
- const indent = getLineIndent(text, firstSpecifier.range[0])
145
152
  const closingIndent = getLineIndent(text, node.range[0])
153
+ const indent = content.includes('\n')
154
+ ? getLineIndent(text, firstSpecifier.range[0])
155
+ : `${closingIndent} `
146
156
 
147
157
  return `${linebreak}${indent}${sortedSpecifiers.join(`,${linebreak}${indent}`)}${linebreak}${closingIndent}`
148
158
  }
@@ -200,13 +210,18 @@ export default {
200
210
  }
201
211
 
202
212
  const unorderedPair = getFirstUnorderedPair(items)
213
+ const sourceCode = context.sourceCode
214
+ const range = getBracesRange(node, items, sourceCode)
215
+ const invalidMultiline = range && shouldBeMultiline(
216
+ sourceCode,
217
+ range,
218
+ items
219
+ )
203
220
 
204
- if (!unorderedPair) {
221
+ if (!unorderedPair && !invalidMultiline) {
205
222
  return
206
223
  }
207
224
 
208
- const sourceCode = context.sourceCode
209
- const range = getBracesRange(node, items, sourceCode)
210
225
  const fix = range && !hasInnerComments(sourceCode, range)
211
226
  ? fixer => fixer.replaceTextRange(
212
227
  range,
@@ -216,11 +231,16 @@ export default {
216
231
  const [
217
232
  previousItem,
218
233
  item
219
- ] = unorderedPair
234
+ ] = unorderedPair ?? [
235
+ items[0],
236
+ items[1]
237
+ ]
220
238
 
221
239
  context.report({
222
240
  node: item.specifier,
223
- message: getMessage(previousItem, item),
241
+ message: unorderedPair
242
+ ? getMessage(previousItem, item)
243
+ : 'Expected named imports to be multiline.',
224
244
  fix
225
245
  })
226
246
  }
@@ -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',