|
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