Skip to content

Commit

Permalink
feat: add quick fix for the compatibility issue that a type is missing
Browse files Browse the repository at this point in the history
  • Loading branch information
Yogu committed Aug 12, 2024
1 parent 18bfe66 commit 27b3db3
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 18 deletions.
35 changes: 29 additions & 6 deletions spec/model/compatibility-check/check-model.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import gql from 'graphql-tag';
import {
expectQuickFix,
expectSingleCompatibilityIssue,
expectToBeValid
expectToBeValid,
} from '../implementation/validation-utils';
import { runCheck } from './utils';
import { Project, ProjectSource } from '../../../core-exports';
import { print } from 'graphql';

describe('checkModel', () => {
describe('basics', () => {
Expand All @@ -24,22 +27,42 @@ describe('checkModel', () => {
});

it('rejects if a type is missing', () => {
const projectToCheck = new Project([
new ProjectSource(
// use the same name as in the baseline project to test the case where the quickfix appends it to the file
'baseline.graphql',
print(gql`
type WrongTypeName @rootEntity {
field: String
}
`),
),
]);
const result = runCheck(
gql`
type Test @rootEntity @modules(in: "module1") {
field: String @modules(all: true)
}
`,
gql`
type WrongTypeName @rootEntity {
field: String
}
`,
projectToCheck,
);
expectSingleCompatibilityIssue(
result,
'Type "Test" is missing (required by module "module1").',
);
expectQuickFix(
result,
'Add type "Test"',
`type WrongTypeName @rootEntity {
field: String
}
type Test @rootEntity {
field: String
}
`,
{ project: projectToCheck },
);
});

it('rejects if a type is of a completely wrong kind', () => {
Expand Down
17 changes: 9 additions & 8 deletions spec/model/compatibility-check/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { DocumentNode, print } from 'graphql';
import { ValidationResult } from '../../../src/model/validation/result';
import { Project } from '../../../src/project/project';
import { ProjectSource } from '../../../src/project/source';
import {
expectNoErrors,
expectToBeValid
} from '../implementation/validation-utils';
import { expectNoErrors, expectToBeValid } from '../implementation/validation-utils';

interface RunOptions {
allowWarningsAndInfosInProjectToCheck?: boolean;
Expand All @@ -14,12 +11,16 @@ interface RunOptions {

export function runCheck(
baselineDoc: DocumentNode,
docToCheck: DocumentNode,
docToCheck: DocumentNode | Project,
options: RunOptions = {},
): ValidationResult {
const projectToCheck = new Project({
sources: [new ProjectSource('to-check.graphql', print(docToCheck))],
});
// allow this to be a Project in case the caller needs this for applyChangeSet
const projectToCheck =
docToCheck instanceof Project
? docToCheck
: new Project({
sources: [new ProjectSource('to-check.graphql', print(docToCheck))],
});
if (options.allowWarningsAndInfosInProjectToCheck) {
expectNoErrors(projectToCheck);
} else {
Expand Down
25 changes: 22 additions & 3 deletions spec/model/implementation/validation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,20 @@ export function expectSingleMessage(component: Validatable, errorPart: string, s
expect(message.severity).to.equal(severity);
}

export interface ExpectQuickFixOptions {
/**
* Optionally, the project that was validated
*
* Needed if the quick fix change set is not just a simple TextChange on a single file
*/
readonly project?: Project;
}

export function expectQuickFix(
component: Validatable,
expectedDescription: string,
expectedBody: string,
{ project }: ExpectQuickFixOptions = {},
) {
const result = validate(component);
const quickfixes = result.messages.flatMap((m) => m.quickFixes);
Expand All @@ -76,10 +86,19 @@ export function expectQuickFix(
const quickfix = matchingQuickfixes[0];
const changeSet = quickfix.getChangeSet();
expect(changeSet.changes).not.to.be.empty;
// dirty hack that works because our quickfix tests only use one file
const project = new Project([changeSet.changes[0].location.source]);

// in most simple test cases, we can recreate the project from the quick fix
if (!project) {
expect(changeSet.appendChanges).to.be.empty;
const source = changeSet.textChanges[0].source;
for (const change of changeSet.textChanges) {
expect(change.source).to.equal(source);
}
project = new Project([source]);
}

const newProject = applyChangeSet(project, changeSet);
expectToBeValid(newProject); // expect the fix to acutally fix the issues
expectToBeValid(newProject); // expect the fix to actually fix the issues
expect(newProject.sources).to.have.a.lengthOf(1);
expect(newProject.sources[0].body).to.equal(expectedBody);
}
41 changes: 40 additions & 1 deletion src/model/compatibility-check/check-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Kind, ObjectTypeDefinitionNode, print, TypeDefinitionNode } from 'graphql';

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 22.x)

Property 'project' does not exist on type 'Model'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 20.x)

Property 'project' does not exist on type 'Model'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 20.x)

Property 'project' does not exist on type 'Model'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 22.x)

Property 'project' does not exist on type 'Model'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 18.x)

Property 'project' does not exist on type 'Model'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 22.x)

Property 'project' does not exist on type 'Model'.
import { MODULES_DIRECTIVE } from '../../schema/constants';
import { AppendChange, ChangeSet } from '../change-set/change-set';
import { Model } from '../implementation';
import { ValidationResult, ValidationMessage, ValidationContext } from '../validation';
import { QuickFix, ValidationContext, ValidationMessage, ValidationResult } from '../validation';
import { checkType } from './check-type';
import { getRequiredBySuffix } from './describe-module-specification';

Expand All @@ -12,10 +15,46 @@ export function checkModel(modelToCheck: Model, baselineModel: Model): Validatio
for (const baselineType of baselineModel.types) {
const matchingType = modelToCheck.getType(baselineType.name);
if (!matchingType) {
const quickFixes: QuickFix[] = [];
if (baselineType.astNode && modelToCheck.project) {
let cleanedAstNode: TypeDefinitionNode = {
...baselineType.astNode,
directives: (baselineType.astNode.directives ?? []).filter(
(d) => d.name.value !== MODULES_DIRECTIVE,
),
};
if (
baselineType.astNode.kind === Kind.OBJECT_TYPE_DEFINITION &&
cleanedAstNode.kind === Kind.OBJECT_TYPE_DEFINITION
) {
cleanedAstNode = {
...cleanedAstNode,
fields: baselineType.astNode.fields?.map((fieldDef) => ({
...fieldDef,
directives: (fieldDef.directives ?? []).filter(
(d) => d.name.value !== MODULES_DIRECTIVE,
),
})),
};
}
const sourceName = baselineType.astNode.loc?.source.name ?? 'new.graphqls';

quickFixes.push(
new QuickFix({
description: `Add type "${baselineType.name}"`,
isPreferred: true,
changeSet: new ChangeSet([
new AppendChange(sourceName, print(cleanedAstNode) + '\n'),
]),
}),
);
}

context.addMessage(
ValidationMessage.compatibilityIssue(
`Type "${baselineType.name}" is missing${getRequiredBySuffix(baselineType)}.`,
undefined,
{ quickFixes },
),
);
continue;
Expand Down

0 comments on commit 27b3db3

Please sign in to comment.