|
1 | 1 | import { ASTKindToNode, Kind, NameNode } from 'graphql'; |
2 | 2 | import { GraphQLESLintRule, ValueOf } from '../types'; |
3 | | -import { TYPES_KINDS, getLocation } from '../utils'; |
| 3 | +import { TYPES_KINDS, getLocation, convertCase } from '../utils'; |
4 | 4 | import { GraphQLESTreeNode } from '../estree-parser'; |
5 | 5 | import { GraphQLESLintRuleListener } from '../testkit'; |
6 | 6 |
|
@@ -51,7 +51,7 @@ type PropertySchema = { |
51 | 51 |
|
52 | 52 | type Options = AllowedStyle | PropertySchema; |
53 | 53 |
|
54 | | -type NamingConventionRuleConfig = { |
| 54 | +export type NamingConventionRuleConfig = { |
55 | 55 | allowLeadingUnderscore?: boolean; |
56 | 56 | allowTrailingUnderscore?: boolean; |
57 | 57 | types?: Options; |
@@ -165,6 +165,7 @@ const rule: GraphQLESLintRule<[NamingConventionRuleConfig]> = { |
165 | 165 | ], |
166 | 166 | }, |
167 | 167 | }, |
| 168 | + hasSuggestions: true, |
168 | 169 | schema: { |
169 | 170 | definitions: { |
170 | 171 | asString: { |
@@ -243,69 +244,97 @@ const rule: GraphQLESLintRule<[NamingConventionRuleConfig]> = { |
243 | 244 | return typeof style === 'object' ? style : { style }; |
244 | 245 | } |
245 | 246 |
|
246 | | - const checkNode = (selector: string) => (node: GraphQLESTreeNode<ValueOf<AllowedKindToNode>>) => { |
247 | | - const { name } = node.kind === Kind.VARIABLE_DEFINITION ? node.variable : node; |
248 | | - if (!name) { |
| 247 | + const checkNode = (selector: string) => (n: GraphQLESTreeNode<ValueOf<AllowedKindToNode>>) => { |
| 248 | + const { name: node } = n.kind === Kind.VARIABLE_DEFINITION ? n.variable : n; |
| 249 | + if (!node) { |
249 | 250 | return; |
250 | 251 | } |
251 | 252 | const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style } = normalisePropertyOption(selector); |
252 | | - const nodeType = KindToDisplayName[node.kind] || node.kind; |
253 | | - const nodeName = name.value; |
254 | | - const errorMessage = getErrorMessage(); |
255 | | - if (errorMessage) { |
| 253 | + const nodeType = KindToDisplayName[n.kind] || n.kind; |
| 254 | + const nodeName = node.value; |
| 255 | + const error = getError(); |
| 256 | + if (error) { |
| 257 | + const { errorMessage, renameToName } = error; |
| 258 | + const [leadingUnderscore] = nodeName.match(/^_*/); |
| 259 | + const [trailingUnderscore] = nodeName.match(/_*$/); |
| 260 | + const suggestedName = leadingUnderscore + renameToName + trailingUnderscore; |
256 | 261 | context.report({ |
257 | | - loc: getLocation(name.loc, name.value), |
| 262 | + loc: getLocation(node.loc, node.value), |
258 | 263 | message: `${nodeType} "${nodeName}" should ${errorMessage}`, |
| 264 | + suggest: [ |
| 265 | + { |
| 266 | + desc: `Rename to "${suggestedName}"`, |
| 267 | + fix: fixer => fixer.replaceText(node as any, suggestedName), |
| 268 | + }, |
| 269 | + ], |
259 | 270 | }); |
260 | 271 | } |
261 | 272 |
|
262 | | - function getErrorMessage(): string | void { |
263 | | - let name = nodeName; |
264 | | - if (allowLeadingUnderscore) { |
265 | | - name = name.replace(/^_*/, ''); |
266 | | - } |
267 | | - if (allowTrailingUnderscore) { |
268 | | - name = name.replace(/_*$/, ''); |
269 | | - } |
| 273 | + function getError(): { |
| 274 | + errorMessage: string; |
| 275 | + renameToName: string; |
| 276 | + } | void { |
| 277 | + const name = nodeName.replace(/(^_+)|(_+$)/g, ''); |
270 | 278 | if (prefix && !name.startsWith(prefix)) { |
271 | | - return `have "${prefix}" prefix`; |
| 279 | + return { |
| 280 | + errorMessage: `have "${prefix}" prefix`, |
| 281 | + renameToName: prefix + name, |
| 282 | + }; |
272 | 283 | } |
273 | 284 | if (suffix && !name.endsWith(suffix)) { |
274 | | - return `have "${suffix}" suffix`; |
| 285 | + return { |
| 286 | + errorMessage: `have "${suffix}" suffix`, |
| 287 | + renameToName: name + suffix, |
| 288 | + }; |
275 | 289 | } |
276 | 290 | const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix)); |
277 | 291 | if (forbiddenPrefix) { |
278 | | - return `not have "${forbiddenPrefix}" prefix`; |
| 292 | + return { |
| 293 | + errorMessage: `not have "${forbiddenPrefix}" prefix`, |
| 294 | + renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''), |
| 295 | + }; |
279 | 296 | } |
280 | 297 | const forbiddenSuffix = forbiddenSuffixes?.find(suffix => name.endsWith(suffix)); |
281 | 298 | if (forbiddenSuffix) { |
282 | | - return `not have "${forbiddenSuffix}" suffix`; |
283 | | - } |
284 | | - if (style && !ALLOWED_STYLES.includes(style)) { |
285 | | - return `be in one of the following options: ${ALLOWED_STYLES.join(', ')}`; |
| 299 | + return { |
| 300 | + errorMessage: `not have "${forbiddenSuffix}" suffix`, |
| 301 | + renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''), |
| 302 | + }; |
286 | 303 | } |
287 | 304 | const caseRegex = StyleToRegex[style]; |
288 | 305 | if (caseRegex && !caseRegex.test(name)) { |
289 | | - return `be in ${style} format`; |
| 306 | + return { |
| 307 | + errorMessage: `be in ${style} format`, |
| 308 | + renameToName: convertCase(style, name), |
| 309 | + }; |
290 | 310 | } |
291 | 311 | } |
292 | 312 | }; |
293 | 313 |
|
294 | | - const checkUnderscore = (node: GraphQLESTreeNode<NameNode>) => { |
| 314 | + const checkUnderscore = (isLeading: boolean) => (node: GraphQLESTreeNode<NameNode>) => { |
295 | 315 | const name = node.value; |
| 316 | + const renameToName = name.replace(new RegExp(isLeading ? '^_+' : '_+$'), ''); |
296 | 317 | context.report({ |
297 | 318 | loc: getLocation(node.loc, name), |
298 | | - message: `${name.startsWith('_') ? 'Leading' : 'Trailing'} underscores are not allowed`, |
| 319 | + message: `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, |
| 320 | + suggest: [ |
| 321 | + { |
| 322 | + desc: `Rename to "${renameToName}"`, |
| 323 | + fix: fixer => fixer.replaceText(node as any, renameToName), |
| 324 | + }, |
| 325 | + ], |
299 | 326 | }); |
300 | 327 | }; |
301 | 328 |
|
302 | 329 | const listeners: GraphQLESLintRuleListener = {}; |
303 | 330 |
|
304 | 331 | if (!allowLeadingUnderscore) { |
305 | | - listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore; |
| 332 | + listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = |
| 333 | + checkUnderscore(true); |
306 | 334 | } |
307 | 335 | if (!allowTrailingUnderscore) { |
308 | | - listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = checkUnderscore; |
| 336 | + listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] = |
| 337 | + checkUnderscore(false); |
309 | 338 | } |
310 | 339 |
|
311 | 340 | const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean)); |
|
0 commit comments