Introducing graphql-lint and gqlint

Yoav LaviYoav Lavi
Introducing graphql-lint and gqlint

Introducing graphql-lint and gqlint

Today we're introducing a new open-source Rust based tool for the GraphQL ecosystem -

When we started implementing our platform linter for GraphQL schemas, we noticed that in contrast to the JavaScript and TypeScript ecosystems (where many new tools are now written in Rust and greatly accelerate the developer feedback loop and CI runtimes), there currently doesn't exist a widely adopted Rust-native linter for GraphQL.

To this end we've decided to open-source and offer as a separate crate our GraphQL linting solution, graphql-lint and its CLI - gqlint.

We hope that by extending and allowing free using of this foundation the GraphQL ecosystem as a whole will benefit.

graphql-lint can lint an SDL schema in a few microseconds (25.953µs for example, for our test schema benchmark) and would fit perfectly into scenarios like a language server in the future for instant in-editor feedback. For a rough comparison, we benchmarked running a JS based linter (with a comparable configuration) in a relatively fast JS runtime at 559.7ms. That means you could lint an SDL schema 21,565 times in the time it took to run the JS based linter, or in other words, a 21,565X performance improvement.

For larger schemas (2533 LOC) we saw 454.79µs vs 698.4ms, a 1,535X improvement.

For parsing the schema and producing an AST graphql-lint uses the great cynic-parser, make sure to check it out as well.

Our first iteration of graphql-lint supports the following:

  • Naming conventions
    • Types: PascalCase
      • Forbidden prefixes: "Type"
      • Forbidden suffixes: "Type"
    • Fields: camelCase
    • Input values: camelCase
    • Arguments: camelCase
    • Directives: camelCase
    • Enums: PascalCase
      • Forbidden prefixes: "Enum"
      • Forbidden suffixes: "Enum"
    • Unions
      • Forbidden prefixes: "Union"
      • Forbidden suffixes: "Union"
    • Enum values: SCREAMING_SNAKE_CASE
    • Interfaces
      • Forbidden prefixes: "Interface"
      • Forbidden suffixes: "Interface"
    • Query fields
      • Forbidden prefixes: ["query", "get", "list"]
      • Forbidden suffixes: "Query"
    • Mutation fields
      • Forbidden prefixes: ["mutation", "put", "post", "patch"]
      • Forbidden suffixes: "Mutation"
    • Subscription fields
      • Forbidden prefixes: "subscription"
      • Forbidden suffixes: "Subscription"
  • Usage of the @deprecated directive requires specifying the reason argument

Future versions will bring with them additional rules and the ability to configure the linter.

To add graphql-lint to your project, add the following to your Cargo.toml:

[dependencies] graphql-lint = "0.1.3"

Or run cargo add graphql-lint in your project, and make use of the exported lint function as follows:

use graphql_lint::lint; fn main () { let schema = r#" type Query { hello: String! } "#; let violations = lint(schema).unwrap(); }

This usage will change as we add new features and configurability.

gqlint is a CLI that runs graphql-lint for command line use.

You can currently install it using cargo by running:

cargo install gqlint

And use it as follows:

$ gqlint schema.graphql ⚠️ [Warning]: directive 'WithDeprecatedArgs' should be renamed to 'withDeprecatedArgs' ⚠️ [Warning]: argument 'ARG' on directive 'WithDeprecatedArgs' should be renamed to 'arg' ⚠️ [Warning]: enum 'Enum_lowercase' should be renamed to 'EnumLowercase' ⚠️ [Warning]: enum 'Enum_lowercase' has a forbidden prefix: 'Enum' ⚠️ [Warning]: usage of directive 'deprecated' on enum 'Enum_lowercase' does not populate the 'reason' argument ⚠️ [Warning]: value 'an_enum_member' on enum 'Enum_lowercase' should be renamed to 'AN_ENUM_MEMBER' ⚠️ [Warning]: usage of directive 'deprecated' on enum value 'an_enum_member' on enum 'Enum_lowercase' does not populate the 'reason' argument ⚠️ [Warning]: enum 'lowercase_Enum' should be renamed to 'LowercaseEnum' ⚠️ [Warning]: enum 'lowercase_Enum' has a forbidden suffix: 'Enum' ⚠️ [Warning]: value 'an_enum_member' on enum 'lowercase_Enum' should be renamed to 'AN_ENUM_MEMBER' ⚠️ [Warning]: usage of directive 'deprecated' on enum value 'an_enum_member' on enum 'lowercase_Enum' does not populate the 'reason' argument ⚠️ [Warning]: field 'getHello' on type 'Query' has a forbidden prefix: 'get' ⚠️ [Warning]: field 'queryHello' on type 'Query' has a forbidden prefix: 'query' ⚠️ [Warning]: field 'listHello' on type 'Query' has a forbidden prefix: 'list' ⚠️ [Warning]: field 'helloQuery' on type 'Query' has a forbidden suffix: 'Query' ⚠️ [Warning]: field 'putHello' on type 'Mutation' has a forbidden prefix: 'put' ⚠️ [Warning]: field 'mutationHello' on type 'Mutation' has a forbidden prefix: 'mutation' ⚠️ [Warning]: field 'postHello' on type 'Mutation' has a forbidden prefix: 'post' ⚠️ [Warning]: field 'patchHello' on type 'Mutation' has a forbidden prefix: 'patch' ⚠️ [Warning]: field 'helloMutation' on type 'Mutation' has a forbidden suffix: 'Mutation' ⚠️ [Warning]: field 'subscriptionHello' on type 'Subscription' has a forbidden prefix: 'subscription' ⚠️ [Warning]: field 'helloSubscription' on type 'Subscription' has a forbidden suffix: 'Subscription' ⚠️ [Warning]: type 'TypeTest' has a forbidden prefix: 'Type' ⚠️ [Warning]: usage of directive 'deprecated' on field 'name' on type 'TypeTest' does not populate the 'reason' argument ⚠️ [Warning]: type 'TestType' has a forbidden suffix: 'Type' ⚠️ [Warning]: type 'other' should be renamed to 'Other' ⚠️ [Warning]: usage of directive 'deprecated' on scalar 'CustomScalar' does not populate the 'reason' argument ⚠️ [Warning]: union 'UnionTest' has a forbidden prefix: 'Union' ⚠️ [Warning]: usage of directive 'deprecated' on union 'UnionTest' does not populate the 'reason' argument ⚠️ [Warning]: union 'TestUnion' has a forbidden suffix: 'Union' ⚠️ [Warning]: interface 'GameInterface' has a forbidden suffix: 'Interface' ⚠️ [Warning]: usage of directive 'deprecated' on field 'publisher' on interface 'GameInterface' does not populate the 'reason' argument ⚠️ [Warning]: interface 'InterfaceGame' has a forbidden prefix: 'Interface' ⚠️ [Warning]: usage of directive 'deprecated' on interface 'InterfaceGame' does not populate the 'reason' argument ⚠️ [Warning]: usage of directive 'deprecated' on input 'TEST' does not populate the 'reason' argument ⚠️ [Warning]: input value 'OTHER' on input 'TEST' should be renamed to 'other' ⚠️ [Warning]: usage of directive 'deprecated' on input value 'OTHER' on input 'TEST' does not populate the 'reason' argument ⚠️ [Warning]: type 'hello' should be renamed to 'Hello' ⚠️ [Warning]: usage of directive 'deprecated' on type 'hello' does not populate the 'reason' argument ⚠️ [Warning]: field 'Test' on type 'hello' should be renamed to 'test' ⚠️ [Warning]: argument 'NAME' on field 'Test' on type 'hello' should be renamed to 'name' ⚠️ [Warning]: type 'hello' should be renamed to 'Hello' ⚠️ [Warning]: field 'GOODBYE' on type 'hello' should be renamed to 'goodbye'