@@ -3,7 +3,13 @@ import { FromSchema } from 'json-schema-to-ts';
33import { GraphQLESTreeNode } from '../estree-converter/index.js' ;
44import { GraphQLESLintRuleListener } from '../testkit.js' ;
55import { 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
814const 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