Skip to content

Commit afea5fe

Browse files
Add requiredPrefixes and requiredSuffixes to naming-convention (#1540)
Co-authored-by: Dimitri POSTOLOV <[email protected]>
1 parent 81f5684 commit afea5fe

File tree

4 files changed

+401
-23
lines changed

4 files changed

+401
-23
lines changed

.changeset/green-vans-leave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
[naming-convention]: add new options `requiredPrefixes`, `requiredSuffixes`

packages/plugin/src/rules/naming-convention.ts

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { FromSchema } from 'json-schema-to-ts';
33
import { GraphQLESTreeNode } from '../estree-converter/index.js';
44
import { GraphQLESLintRuleListener } from '../testkit.js';
55
import { GraphQLESLintRule, ValueOf } from '../types.js';
6-
import { ARRAY_DEFAULT_OPTIONS, convertCase, truthy, TYPES_KINDS } from '../utils.js';
6+
import {
7+
ARRAY_DEFAULT_OPTIONS,
8+
convertCase,
9+
englishJoinWords,
10+
truthy,
11+
TYPES_KINDS,
12+
} from '../utils.js';
713

814
const KindToDisplayName = {
915
// types
@@ -57,6 +63,8 @@ const schema = {
5763
suffix: { type: 'string' },
5864
forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
5965
forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
66+
requiredPrefixes: ARRAY_DEFAULT_OPTIONS,
67+
requiredSuffixes: ARRAY_DEFAULT_OPTIONS,
6068
ignorePattern: {
6169
type: 'string',
6270
description: 'Option to skip validation of some words, e.g. acronyms',
@@ -113,6 +121,8 @@ type PropertySchema = {
113121
prefix?: string;
114122
forbiddenPrefixes?: string[];
115123
forbiddenSuffixes?: string[];
124+
requiredPrefixes?: string[];
125+
requiredSuffixes?: string[];
116126
ignorePattern?: string;
117127
};
118128

@@ -194,6 +204,46 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
194204
}
195205
`,
196206
},
207+
{
208+
title: 'Correct',
209+
usage: [
210+
{
211+
'FieldDefinition[gqlType.name.value=Boolean]': {
212+
style: 'camelCase',
213+
requiredPrefixes: ['is', 'has'],
214+
},
215+
'FieldDefinition[gqlType.gqlType.name.value=Boolean]': {
216+
style: 'camelCase',
217+
requiredPrefixes: ['is', 'has'],
218+
},
219+
},
220+
],
221+
code: /* GraphQL */ `
222+
type Product {
223+
isBackordered: Boolean
224+
isNew: Boolean!
225+
hasDiscount: Boolean!
226+
}
227+
`,
228+
},
229+
{
230+
title: 'Correct',
231+
usage: [
232+
{
233+
'FieldDefinition[gqlType.gqlType.name.value=SensitiveSecret]': {
234+
style: 'camelCase',
235+
requiredSuffixes: ['SensitiveSecret'],
236+
},
237+
},
238+
],
239+
code: /* GraphQL */ `
240+
scalar SensitiveSecret
241+
242+
type Account {
243+
accountSensitiveSecret: SensitiveSecret!
244+
}
245+
`,
246+
},
197247
],
198248
configOptions: {
199249
schema: [
@@ -250,17 +300,15 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
250300
function report(
251301
node: GraphQLESTreeNode<NameNode>,
252302
message: string,
253-
suggestedName: string,
303+
suggestedNames: string[],
254304
): void {
255305
context.report({
256306
node,
257307
message,
258-
suggest: [
259-
{
260-
desc: `Rename to \`${suggestedName}\``,
261-
fix: fixer => fixer.replaceText(node as any, suggestedName),
262-
},
263-
],
308+
suggest: suggestedNames.map(suggestedName => ({
309+
desc: `Rename to \`${suggestedName}\``,
310+
fix: fixer => fixer.replaceText(node as any, suggestedName),
311+
})),
264312
});
265313
}
266314

@@ -269,22 +317,32 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
269317
if (!node) {
270318
return;
271319
}
272-
const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style, ignorePattern } =
273-
normalisePropertyOption(selector);
320+
const {
321+
prefix,
322+
suffix,
323+
forbiddenPrefixes,
324+
forbiddenSuffixes,
325+
style,
326+
ignorePattern,
327+
requiredPrefixes,
328+
requiredSuffixes,
329+
} = normalisePropertyOption(selector);
274330
const nodeType = KindToDisplayName[n.kind] || n.kind;
275331
const nodeName = node.value;
276332
const error = getError();
277333
if (error) {
278-
const { errorMessage, renameToName } = error;
334+
const { errorMessage, renameToNames } = error;
279335
const [leadingUnderscores] = nodeName.match(/^_*/) as RegExpMatchArray;
280336
const [trailingUnderscores] = nodeName.match(/_*$/) as RegExpMatchArray;
281-
const suggestedName = leadingUnderscores + renameToName + trailingUnderscores;
282-
report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedName);
337+
const suggestedNames = renameToNames.map(
338+
renameToName => leadingUnderscores + renameToName + trailingUnderscores,
339+
);
340+
report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedNames);
283341
}
284342

285343
function getError(): {
286344
errorMessage: string;
287-
renameToName: string;
345+
renameToNames: string[];
288346
} | void {
289347
const name = nodeName.replace(/(^_+)|(_+$)/g, '');
290348
if (ignorePattern && new RegExp(ignorePattern, 'u').test(name)) {
@@ -293,27 +351,53 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
293351
if (prefix && !name.startsWith(prefix)) {
294352
return {
295353
errorMessage: `have "${prefix}" prefix`,
296-
renameToName: prefix + name,
354+
renameToNames: [prefix + name],
297355
};
298356
}
299357
if (suffix && !name.endsWith(suffix)) {
300358
return {
301359
errorMessage: `have "${suffix}" suffix`,
302-
renameToName: name + suffix,
360+
renameToNames: [name + suffix],
303361
};
304362
}
305363
const forbiddenPrefix = forbiddenPrefixes?.find(prefix => name.startsWith(prefix));
306364
if (forbiddenPrefix) {
307365
return {
308366
errorMessage: `not have "${forbiddenPrefix}" prefix`,
309-
renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
367+
renameToNames: [name.replace(new RegExp(`^${forbiddenPrefix}`), '')],
310368
};
311369
}
312370
const forbiddenSuffix = forbiddenSuffixes?.find(suffix => name.endsWith(suffix));
313371
if (forbiddenSuffix) {
314372
return {
315373
errorMessage: `not have "${forbiddenSuffix}" suffix`,
316-
renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
374+
renameToNames: [name.replace(new RegExp(`${forbiddenSuffix}$`), '')],
375+
};
376+
}
377+
if (
378+
requiredPrefixes &&
379+
!requiredPrefixes.some(requiredPrefix => name.startsWith(requiredPrefix))
380+
) {
381+
return {
382+
errorMessage: `have one of the following prefixes: ${englishJoinWords(
383+
requiredPrefixes,
384+
)}`,
385+
renameToNames: style
386+
? requiredPrefixes.map(prefix => convertCase(style, `${prefix} ${name}`))
387+
: requiredPrefixes.map(prefix => `${prefix}${name}`),
388+
};
389+
}
390+
if (
391+
requiredSuffixes &&
392+
!requiredSuffixes.some(requiredSuffix => name.endsWith(requiredSuffix))
393+
) {
394+
return {
395+
errorMessage: `have one of the following suffixes: ${englishJoinWords(
396+
requiredSuffixes,
397+
)}`,
398+
renameToNames: style
399+
? requiredSuffixes.map(suffix => convertCase(style, `${name} ${suffix}`))
400+
: requiredSuffixes.map(suffix => `${name}${suffix}`),
317401
};
318402
}
319403
// Style is optional
@@ -324,19 +408,17 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
324408
if (!caseRegex.test(name)) {
325409
return {
326410
errorMessage: `be in ${style} format`,
327-
renameToName: convertCase(style, name),
411+
renameToNames: [convertCase(style, name)],
328412
};
329413
}
330414
}
331415
};
332416

333417
const checkUnderscore = (isLeading: boolean) => (node: GraphQLESTreeNode<NameNode>) => {
334418
const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, '');
335-
report(
336-
node,
337-
`${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`,
419+
report(node, `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, [
338420
suggestedName,
339-
);
421+
]);
340422
};
341423

342424
const listeners: GraphQLESLintRuleListener = {};

0 commit comments

Comments
 (0)