Skip to content

Commit

Permalink
feat: add deprecated arg detection and tests (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
digiz3d authored Oct 10, 2024
1 parent c42ebb6 commit 75fc02a
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 13 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Any deprecated field will be reported and make the CI fail.

## Usage

You need the operation files (queries/mutations/subscriptions) and the schema file.
You need the operation files (queries/mutations/subscriptions) and the schema file.
[According to the GraphQL spec](https://spec.graphql.org/draft/#sec-Root-Operation-Types), the schema must define at least a field of the Query root type.

### Input parameters

Expand Down Expand Up @@ -49,6 +50,9 @@ If you don't commit the schema, then you might fetch it using rover:
## Tech details
Using [ncc](https://github.com/vercel/ncc) as a bundler
Using [ncc](https://github.com/vercel/ncc) as a bundler.
`dist` is committed be able to run with a `node20` runner without any installation step.

This action does yet not scan object literals. It is fine as they are often replaced by variables in client operation files.
Feel free to submit a PR (with tests) if you would like to support it.
22 changes: 20 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49509,7 +49509,9 @@ const schema = (0,graphql__WEBPACK_IMPORTED_MODULE_2__/* .buildSchema */ .IV5)(s
const deprecatedFields = (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .validateOperationsAndReportDeprecatedFields */ .So)(schema, operationDocuments, fragmentsByName, dependenciesByFragmentName, shouldReportFiles);
if (deprecatedFields.size > 0) {
console.error('Deprecated fields found.');
deprecatedFields.forEach((field) => console.error(field));
Array.from(deprecatedFields)
.toSorted((a, b) => a.localeCompare(b))
.forEach((field) => console.error(field));
process.exit(1);
}
else {
Expand Down Expand Up @@ -57302,12 +57304,28 @@ function validateOperationsAndReportDeprecatedFields(schema, documentsMap, fragm
}
const typeInfo = new graphql/* TypeInfo */.DxK(schema);
(0,graphql/* visit */.YRT)(documentWithFragments, (0,graphql/* visitWithTypeInfo */.Sw7)(typeInfo, {
Argument(node) {
const argDef = typeInfo.getArgument();
if (argDef && argDef.deprecationReason) {
const parentType = typeInfo.getFieldDef();
const parentTypeName = parentType ? parentType.name : 'Unknown Type';
deprecatedFields.add(`Argument "${node.name.value}" from "${parentTypeName}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`);
}
},
Field(node) {
const fieldDef = typeInfo.getFieldDef();
if (fieldDef && fieldDef.deprecationReason) {
const parentType = typeInfo.getParentType();
const parentTypeName = parentType ? parentType.name : 'Unknown Type';
deprecatedFields.add(`"${parentTypeName}.${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`);
switch (parentTypeName) {
case 'Mutation':
case 'Query':
case 'Subscription':
deprecatedFields.add(`${parentTypeName} "${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`);
break;
default:
deprecatedFields.add(`Field "${parentTypeName}.${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`);
}
}
},
}));
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1.5",
"version": "1.0.0",
"name": "lint-graphql-operations",
"module": "dist/index.js",
"type": "module",
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const deprecatedFields = validateOperationsAndReportDeprecatedFields(

if (deprecatedFields.size > 0) {
console.error('Deprecated fields found.')
deprecatedFields.forEach((field) => console.error(field))
Array.from(deprecatedFields)
.toSorted((a, b) => a.localeCompare(b))
.forEach((field) => console.error(field))
process.exit(1)
} else {
console.log('No deprecated fields found. GG!')
Expand Down
26 changes: 23 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,34 @@ export function validateOperationsAndReportDeprecatedFields(
visit(
documentWithFragments,
visitWithTypeInfo(typeInfo, {
Argument(node) {
const argDef = typeInfo.getArgument()
if (argDef && argDef.deprecationReason) {
const parentType = typeInfo.getFieldDef()
const parentTypeName = parentType ? parentType.name : 'Unknown Type'
deprecatedFields.add(
`Argument "${node.name.value}" from "${parentTypeName}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`,
)
}
},
Field(node) {
const fieldDef = typeInfo.getFieldDef()
if (fieldDef && fieldDef.deprecationReason) {
const parentType = typeInfo.getParentType()
const parentTypeName = parentType ? parentType.name : 'Unknown Type'
deprecatedFields.add(
`"${parentTypeName}.${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`,
)
switch (parentTypeName) {
case 'Mutation':
case 'Query':
case 'Subscription':
deprecatedFields.add(
`${parentTypeName} "${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`,
)
break
default:
deprecatedFields.add(
`Field "${parentTypeName}.${node.name.value}" is deprecated${shouldReportFiles ? ` in ${operationFilePath}` : ''}`,
)
}
}
},
}),
Expand Down
22 changes: 20 additions & 2 deletions test/index.ts → test/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ test('should find all deprecated usages', async () => {
dependenciesByFragmentName,
false,
)
assert.equal(reportTypes.size, 3)
assert.equal(reportTypes.size, 4)
assert.deepEqual(
Array.from(reportTypes).toSorted((a, b) => a.localeCompare(b)),
[
'Argument "input" from "someQueryWithDeprecatedInput" is deprecated',
'Field "SomeSubType.subTypeDeepDeprecatedField" is deprecated',
'Field "SomeType.someDeprecatedField" is deprecated',
'Query "someDeprecatedQuery" is deprecated',
],
)

const reportTypesWithFiles = validateOperationsAndReportDeprecatedFields(
schema,
Expand All @@ -39,5 +48,14 @@ test('should find all deprecated usages', async () => {
dependenciesByFragmentName,
true,
)
assert.equal(reportTypesWithFiles.size, 3)
assert.equal(reportTypesWithFiles.size, 4)
assert.deepEqual(
Array.from(reportTypesWithFiles).toSorted((a, b) => a.localeCompare(b)),
[
'Argument "input" from "someQueryWithDeprecatedInput" is deprecated in test/query.graphql',
'Field "SomeSubType.subTypeDeepDeprecatedField" is deprecated in test/query.graphql',
'Field "SomeType.someDeprecatedField" is deprecated in test/query.graphql',
'Query "someDeprecatedQuery" is deprecated in test/query.graphql',
],
)
})
97 changes: 97 additions & 0 deletions test/unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { buildSchema, parse, type DocumentNode } from 'graphql'
import test from 'node:test'
import { validateOperationsAndReportDeprecatedFields } from '../src/utils'
import assert from 'node:assert'

function makeDocumentMap(str: string) {
const documentsMap = new Map<string, DocumentNode>()
documentsMap.set('test.graphql', parse(str))
return documentsMap
}

function buildSchemaWithRootQuery(str: string) {
// GraphQL validation rules imply that a schema must have a Query type
if (str.match(/type\sQuery[^a-zA-Z0-9_]/)) return buildSchema(str)
return buildSchema('type Query { ok: String } ' + str)
}

function assertValid(schemaString: string, queryString: string, expectedResult: string) {
const schema = buildSchemaWithRootQuery(schemaString)
const documentsMap = makeDocumentMap(queryString)
const deprecatedFields = validateOperationsAndReportDeprecatedFields(
schema,
documentsMap,
new Map(),
new Map(),
false,
)
assert.equal(deprecatedFields.size, 1)
assert.equal(deprecatedFields.values().next().value, expectedResult)
}

test('find deprecated query', () => {
assertValid(
`
type Query {
someQuery: String @deprecated(reason: "Use nothing instead")
}
`,
`query Test { someQuery }`,
'Query "someQuery" is deprecated',
)
})

test('find deprecated mutation', () => {
assertValid(
`
type Mutation {
someMutation(input: String!): String! @deprecated(reason: "Use nothing instead")
}
`,
`mutation Test { someMutation(input:"hi") }`,
'Mutation "someMutation" is deprecated',
)
})

test('find deprecated subscription', () => {
assertValid(
`
type Subscription {
someSubscription(input: String!): String! @deprecated(reason: "Use nothing instead")
}
`,
`subscription Test { someSubscription(input:"hi") }`,
'Subscription "someSubscription" is deprecated',
)
})

test('find deprecated field', () => {
assertValid(
`
type SomePayload {
ok: Boolean
notOk: String @deprecated(reason: "Use ok instead")
}
type Query {
someQuery: SomePayload
}
`,
`query Test { someQuery { ok notOk } }`,
'Field "SomePayload.notOk" is deprecated',
)
})

test('find deprecated arg', () => {
assertValid(
`
type SomePayload {
ok: Boolean
}
type Query {
someQuery(arg: String @deprecated(reason: "Stop using it")): SomePayload
}
`,
`query Test { someQuery(arg: "hi") { ok } }`,
'Argument "arg" from "someQuery" is deprecated',
)
})

0 comments on commit 75fc02a

Please sign in to comment.