Skip to content

Commit

Permalink
feat: add quick fix to add @Suppress directive
Browse files Browse the repository at this point in the history
  • Loading branch information
Yogu committed Sep 6, 2024
1 parent 3bb7c41 commit cac7cde
Show file tree
Hide file tree
Showing 17 changed files with 475 additions and 55 deletions.
218 changes: 218 additions & 0 deletions spec/model/validation/suppress/quick-fix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { expectQuickFix } from '../../implementation/validation-utils';
import gql from 'graphql-tag';
import { Project } from '../../../../src/project/project';

describe('@suppress quick fix', () => {
it('generates a quick fix on type level', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
foo: String
}
# comment before type
type Child @childEntity {
# comment in type
stuff: Int
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
foo: String
}
# comment before type
type Child @childEntity @suppress(warnings: UNUSED) {
# comment in type
stuff: Int
}
`,
);
});

it('generates a quick fix on enum level', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
value: myenum
}
enum myenum {
NAME1
NAME2
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
value: myenum
}
enum myenum @suppress(warnings: NAMING) {
NAME1
NAME2
}
`,
);
});

it('generates a quick fix on enum value level', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
value: MyEnum
}
enum MyEnum {
NAME1
name2
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
value: MyEnum
}
enum MyEnum {
NAME1
name2 @suppress(warnings: NAMING)
}
`,
);
});

it('generates a quick fix on field level', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key # after field
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: DEPRECATED) # after field
key: String
}
`,
);
});

it('generates a quick fix if there already is an empty @suppress directive', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key @suppress
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: DEPRECATED)
key: String
}
`,
);
});

it('generates a quick fix if there already is a @suppress directive with a different arg', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key @suppress(infos: NO_TYPE_CHECKS)
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(infos: NO_TYPE_CHECKS, warnings: DEPRECATED)
key: String
}
`,
);
});

it('generates a quick fix if there already is a @suppress directive with a single code', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: UNUSED)
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: [UNUSED, DEPRECATED])
key: String
}
`,
);
});

it('generates a quick fix if there already is a @suppress directive with a single code as a list', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: [UNUSED])
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: [UNUSED, DEPRECATED])
key: String
}
`,
);
});

it('generates a quick fix if there already is a @suppress directive with a multiple codes as a list', () => {
const project = new Project([
gql`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: [UNUSED, NAMING])
key: String
}
`.loc!.source,
]);
expectQuickFix(
project,
'Suppress this warning',
`
type Stuff @rootEntity {
_key: String @key @suppress(warnings: [UNUSED, NAMING, DEPRECATED])
key: String
}
`,
);
});
});
2 changes: 2 additions & 0 deletions src/model/change-set/change-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export class ChangeSet {
this.textChanges = changes.filter((c) => c instanceof TextChange);
this.appendChanges = changes.filter((c) => c instanceof AppendChange);
}

static EMPTY = new ChangeSet([]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/model/create-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ import { parseI18nConfigs } from './parse-i18n';
import { parseModuleConfigs } from './parse-modules';
import { parseTTLConfigs } from './parse-ttl';
import { ValidationContext, ValidationMessage } from './validation';
import { WarningCode } from '../schema/message-codes';
import { WarningCode } from './validation/suppress/message-codes';

export function createModel(parsedProject: ParsedProject, options: ModelOptions = {}): Model {
const validationContext = new ValidationContext();
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/enum-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EnumValueLocalization } from './i18n';
import { Model } from './model';
import { TypeBase } from './type-base';
import memorize from 'memorize-decorator';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export class EnumType extends TypeBase {
constructor(input: EnumTypeConfig, model: Model) {
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { Relation, RelationSide } from './relation';
import { RolesSpecifier } from './roles-specifier';
import { InvalidType, ObjectType, Type } from './type';
import { ValueObjectType } from './value-object-type';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export interface SystemFieldConfig extends FieldConfig {
readonly isSystemField?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/flex-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ModelComponent, ValidationContext } from '../validation/validation-cont
import { FlexSearchPrimarySortClauseConfig } from '../config';
import { RootEntityType } from './root-entity-type';
import { Severity, ValidationMessage } from '../validation';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export const IDENTITY_ANALYZER = 'identity';
export const NORM_CI_ANALYZER = 'norm_ci';
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/roles-specifier.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ModelComponent, ValidationContext } from '../validation/validation-context';
import { RolesSpecifierConfig } from '../config';
import { ValidationMessage } from '../validation';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';
import { Type } from './type';
import { Field } from './field';

Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/root-entity-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { RolesSpecifier } from './roles-specifier';
import { ScalarType } from './scalar-type';
import { TimeToLiveType } from './time-to-live';
import { EffectiveModuleSpecification } from './modules/effective-module-specification';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export class RootEntityType extends ObjectTypeBase {
private readonly permissions: PermissionsConfig & {};
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/time-to-live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RootEntityType } from './root-entity-type';
import { ScalarType } from './scalar-type';
import { Type } from './type';
import { FieldPath } from './field-path';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export class TimeToLiveType implements ModelComponent {
readonly cascadeFields: ReadonlyArray<FieldPath> = [];
Expand Down
2 changes: 1 addition & 1 deletion src/model/implementation/type-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { EffectiveModuleSpecification } from './modules/effective-module-specifi
import { MODULES_DIRECTIVE } from '../../schema/constants';
import { TypeModuleSpecification } from './modules/type-module-specification';
import { Type } from './type';
import { WarningCode } from '../../schema/message-codes';
import { WarningCode } from '../validation/suppress/message-codes';

export abstract class TypeBase implements ModelComponent {
readonly name: string;
Expand Down
69 changes: 25 additions & 44 deletions src/model/validation/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { EnumValueDefinitionNode, FieldDefinitionNode, Kind, TypeDefinitionNode } from 'graphql';
import {
ArgumentNode,
ASTNode,
DirectiveNode,
EnumValueDefinitionNode,
FieldDefinitionNode,
Kind,
ListValueNode,
print,
TypeDefinitionNode,
} from 'graphql';
import {
SUPPRESS_COMPATIBILITY_ISSUES_ARG,
SUPPRESS_DIRECTIVE,
Expand All @@ -10,9 +20,12 @@ import {
InfoCode,
MessageCode,
WarningCode,
} from '../../schema/message-codes';
} from './suppress/message-codes';
import { LocationLike, MessageLocation } from './location';
import { QuickFix } from './quick-fix';
import { ChangeSet, TextChange } from '../change-set/change-set';
import { isSuppressed } from './suppress/is-suppressed';
import { createSuppressQuickFix } from './suppress/quick-fix';

export enum Severity {
ERROR = 'ERROR',
Expand Down Expand Up @@ -70,14 +83,21 @@ export class ValidationMessage {
) {
// not sure if this is the right time to do this
// also does not allow us to detect superfluous directives at the moment
const isSuppressed = calculateIsSuppressed(severity, astNode, code);
const suppressed = isSuppressed(severity, astNode, code);
let quickFixes = options?.quickFixes ?? [];
if (!suppressed && astNode) {
const suppressQuickFix = createSuppressQuickFix(severity, code, astNode);
if (suppressQuickFix) {
quickFixes = [...quickFixes, suppressQuickFix];
}
}
return new ValidationMessage({
severity,
code,
message,
location: options?.location ?? astNode,
isSuppressed,
...options,
isSuppressed: suppressed,
quickFixes,
});
}

Expand Down Expand Up @@ -190,42 +210,3 @@ function severityToString(severity: Severity) {
return 'Compatibility issue';
}
}

function calculateIsSuppressed(
severity: Severity,
location: AstNodeWithDirectives | undefined,
code: MessageCode,
) {
const suppressDirective = location?.directives?.find(
(d) => d.name.value === SUPPRESS_DIRECTIVE,
);
if (!suppressDirective) {
return false;
}
let argName;
switch (severity) {
case Severity.COMPATIBILITY_ISSUE:
argName = SUPPRESS_COMPATIBILITY_ISSUES_ARG;
break;
case Severity.WARNING:
argName = SUPPRESS_WARNINGS_ARG;
break;
case Severity.INFO:
argName = SUPPRESS_INFOS_ARG;
break;
default:
throw new Error(`Non-suppressable severity: ${severity}`);
}
const codesArg = suppressDirective?.arguments?.find((a) => a.name.value === argName);
if (!codesArg) {
return false;
}
if (codesArg.value.kind === Kind.ENUM) {
// you can omit the [] in graphql if it's a single list entry
return codesArg.value.value === code;
}
if (codesArg.value.kind !== Kind.LIST) {
return false;
}
return codesArg.value.values.some((v) => v.kind === Kind.ENUM && v.value === code);
}
Loading

0 comments on commit cac7cde

Please sign in to comment.