Skip to content

Commit

Permalink
Merge branch 'Rash-Hit-#122-__typename-must-be-const---fix'
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Mar 31, 2024
2 parents 7a21f09 + 8167c2d commit 1eed523
Show file tree
Hide file tree
Showing 76 changed files with 407 additions and 105 deletions.
2 changes: 1 addition & 1 deletion examples/express-graphql-http/models/User.ts
Expand Up @@ -4,7 +4,7 @@ import Group from "./Group";

/** @gqlType User */
export default class UserResolver implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name(): string {
return "Alice";
Expand Down
2 changes: 1 addition & 1 deletion examples/express-graphql/models/User.ts
Expand Up @@ -4,7 +4,7 @@ import Group from "./Group";

/** @gqlType User */
export default class UserResolver implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name(): string {
return "Alice";
Expand Down
2 changes: 1 addition & 1 deletion examples/next-js/app/api/graphql/models/User.ts
Expand Up @@ -4,7 +4,7 @@ import Group from "./Group";

/** @gqlType User */
export default class UserResolver implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name(): string {
return "Alice";
Expand Down
2 changes: 1 addition & 1 deletion examples/production-app/models/Like.ts
Expand Up @@ -12,7 +12,7 @@ import { Post } from "./Post";
* A reaction from a user indicating that they like a post.
* @gqlType */
export class Like extends Model<DB.LikeRow> implements GraphQLNode {
__typename = "Like";
__typename = "Like" as const;

/**
* The date and time at which the post was liked.
Expand Down
2 changes: 1 addition & 1 deletion examples/production-app/models/Post.ts
Expand Up @@ -13,7 +13,7 @@ import { connectionFromArray } from "graphql-relay";
* A blog post.
* @gqlType */
export class Post extends Model<DB.PostRow> implements GraphQLNode {
__typename = "Post";
__typename = "Post" as const;

/**
* The editor-approved title of the post.
Expand Down
2 changes: 1 addition & 1 deletion examples/production-app/models/User.ts
Expand Up @@ -9,7 +9,7 @@ import { Connection } from "../graphql/Connection";

/** @gqlType */
export class User extends Model<DB.UserRow> implements GraphQLNode {
__typename = "User";
__typename = "User" as const;

/**
* User's name. **Note:** This field is not guaranteed to be unique.
Expand Down
2 changes: 1 addition & 1 deletion examples/strict-semantic-nullability/models/User.ts
Expand Up @@ -4,7 +4,7 @@ import Group from "./Group";

/** @gqlType User */
export default class UserResolver implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name(): string {
return "Alice";
Expand Down
2 changes: 1 addition & 1 deletion examples/yoga/models/User.ts
Expand Up @@ -4,7 +4,7 @@ import Group from "./Group";

/** @gqlType User */
export default class UserResolver implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name(): string {
return "Alice";
Expand Down
38 changes: 34 additions & 4 deletions src/Errors.ts
Expand Up @@ -152,14 +152,44 @@ export function typeNameNotDeclaration() {
}

const TYPENAME_CONTEXT =
"This lets Grats know that the GraphQL executor will be able to derive the type of the object at runtime.";
"This is needed to ensure Grats can determine the type of this object during GraphQL execution.";

function _typeNamePropertyExample(expectedName: string): string {
return `For example: \`__typename = "${expectedName}" as const\` or \`__typename: "${expectedName}";\`.`;
}

export function typeNameMissingInitializer() {
return `Expected \`__typename\` property to have an initializer or a string literal type. For example: \`__typename = "MyType"\` or \`__typename: "MyType";\`. ${TYPENAME_CONTEXT}`;
return `Expected \`__typename\` property to have an initializer or a string literal type. ${TYPENAME_CONTEXT}`;
}

export function typeNameInitializeNotString(expectedName: string) {
return `Expected \`__typename\` property initializer to be a string literal. ${_typeNamePropertyExample(
expectedName,
)} ${TYPENAME_CONTEXT}`;
}

export function typeNameInitializeNotExpression(expectedName: string) {
return `Expected \`__typename\` property initializer to be an expression with a const assertion. ${_typeNamePropertyExample(
expectedName,
)} ${TYPENAME_CONTEXT}`;
}

export function typeNameTypeNotReferenceNode(expectedName: string) {
return `Expected \`__typename\` property must be correctly defined. ${_typeNamePropertyExample(
expectedName,
)} ${TYPENAME_CONTEXT}`;
}

export function typeNameTypeNameNotIdentifier(expectedName: string) {
return `Expected \`__typename\` property name must be correctly specified. ${_typeNamePropertyExample(
expectedName,
)} ${TYPENAME_CONTEXT}`;
}

export function typeNameInitializeNotString() {
return `Expected \`__typename\` property initializer to be a string literal. For example: \`__typename = "MyType"\` or \`__typename: "MyType";\`. ${TYPENAME_CONTEXT}`;
export function typeNameTypeNameNotConst(expectedName: string) {
return `Expected \`__typename\` property type name to be "const". ${_typeNamePropertyExample(
expectedName,
)} ${TYPENAME_CONTEXT}`;
}

export function typeNameInitializerWrong(expected: string, actual: string) {
Expand Down
110 changes: 99 additions & 11 deletions src/Extractor.ts
Expand Up @@ -827,7 +827,7 @@ class Extractor {
isValidTypeNameProperty(
member: ts.ClassElement | ts.TypeElement,
expectedName: string,
) {
): boolean {
if (
member.name == null ||
!ts.isIdentifier(member.name) ||
Expand All @@ -851,51 +851,139 @@ class Extractor {
isValidTypenamePropertyDeclaration(
node: ts.PropertyDeclaration,
expectedName: string,
) {
): boolean {
// If we have a type annotation, we ask that it be a string literal.
// That means, that if we have one, _and_ it's valid, we're done.
// Otherwise we fall through to the initializer check.
if (node.type != null) {
return this.isValidTypenamePropertyType(node.type, expectedName);
}
if (node.initializer == null) {
this.report(node.name, E.typeNameMissingInitializer());
this.report(
node.name,
E.typeNameMissingInitializer(),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (!ts.isStringLiteral(node.initializer)) {
this.report(node.initializer, E.typeNameInitializeNotString());
if (!ts.isAsExpression(node.initializer)) {
this.report(
node.initializer,
E.typeNameInitializeNotExpression(expectedName),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (node.initializer.text !== expectedName) {
if (!ts.isStringLiteral(node.initializer.expression)) {
this.report(
node.initializer,
E.typeNameInitializerWrong(expectedName, node.initializer.text),
node.initializer.expression,
E.typeNameInitializeNotString(expectedName),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (node.initializer.expression.text !== expectedName) {
this.report(
node.initializer.expression,
E.typeNameInitializerWrong(
expectedName,
node.initializer.expression.text,
),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (!ts.isTypeReferenceNode(node.initializer.type)) {
this.report(
node.initializer.type,
E.typeNameTypeNotReferenceNode(expectedName),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (!ts.isIdentifier(node.initializer.type.typeName)) {
this.report(
node.initializer.type.typeName,
E.typeNameTypeNameNotIdentifier(expectedName),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

if (node.initializer.type.typeName.escapedText !== "const") {
this.report(
node.initializer.type.typeName,
E.typeNameTypeNameNotConst(expectedName),
[],
this.fixTypenameProperty(node, expectedName),
);
return false;
}

return true;
}

fixTypenameProperty(node: ts.Node, expectedName: string): ts.CodeFixAction {
return {
fixName: "fix-typename-property",
description: "Create Grats-compatible `__typename` property",
changes: [
Act.replaceNode(node, `__typename = "${expectedName}" as const;`),
],
};
}

fixTypenameType(node: ts.Node, expectedName: string): ts.CodeFixAction {
return {
fixName: "fix-typename-type",
description: "Create Grats-compatible `__typename` type",
changes: [Act.replaceNode(node, `"${expectedName}"`)],
};
}

isValidTypenamePropertySignature(
node: ts.PropertySignature,
expectedName: string,
) {
if (node.type == null) {
this.report(node, E.typeNameMissingTypeAnnotation(expectedName));
this.report(node, E.typeNameMissingTypeAnnotation(expectedName), [], {
fixName: "add-typename-type",
description: "Add Grats-compatible `__typename` type",
changes: [Act.suffixNode(node, `: "${expectedName}"`)],
});
return false;
}
return this.isValidTypenamePropertyType(node.type, expectedName);
}

isValidTypenamePropertyType(node: ts.TypeNode, expectedName: string) {
if (!ts.isLiteralTypeNode(node) || !ts.isStringLiteral(node.literal)) {
this.report(node, E.typeNameTypeNotStringLiteral(expectedName));
this.report(
node,
E.typeNameTypeNotStringLiteral(expectedName),
[],
this.fixTypenameType(node, expectedName),
);
return false;
}
if (node.literal.text !== expectedName) {
this.report(node, E.typeNameDoesNotMatchExpected(expectedName));
this.report(
node,
E.typeNameDoesNotMatchExpected(expectedName),
[],
this.fixTypenameType(node, expectedName),
);
return false;
}
return true;
Expand Down
Expand Up @@ -9,7 +9,7 @@ interface IPerson {

/** @gqlType */
export class User implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
// @ts-ignore
name(): string | null {
Expand Down
Expand Up @@ -12,7 +12,7 @@ interface IPerson {

/** @gqlType */
export class User implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
// @ts-ignore
name(): string | null {
Expand Down
4 changes: 2 additions & 2 deletions src/tests/fixtures/field_values/UnionField.ts
Expand Up @@ -6,14 +6,14 @@ export default class SomeType {

/** @gqlType */
class User {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}

/** @gqlType */
class Entity {
__typename = "Entity";
__typename = "Entity" as const;
/** @gqlField */
description: string;
}
Expand Down
4 changes: 2 additions & 2 deletions src/tests/fixtures/field_values/UnionField.ts.expected
Expand Up @@ -9,14 +9,14 @@ export default class SomeType {

/** @gqlType */
class User {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}

/** @gqlType */
class Entity {
__typename = "Entity";
__typename = "Entity" as const;
/** @gqlField */
description: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/interfaces/FieldReturnsInterface.ts
Expand Up @@ -14,7 +14,7 @@ interface IPerson {

/** @gqlType */
class User implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Up @@ -17,7 +17,7 @@ interface IPerson {

/** @gqlType */
class User implements IPerson {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/interfaces/IgnoresExtendsClause.ts
Expand Up @@ -18,7 +18,7 @@ interface Actor {

/** @gqlType */
class User extends Person implements Actor {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Up @@ -21,7 +21,7 @@ interface Actor {

/** @gqlType */
class User extends Person implements Actor {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fixtures/interfaces/ImplementsInterface.ts
Expand Up @@ -14,7 +14,7 @@ interface Person {

/** @gqlType */
class User implements Person {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Up @@ -17,7 +17,7 @@ interface Person {

/** @gqlType */
class User implements Person {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;
}
Expand Down
Expand Up @@ -16,7 +16,7 @@ interface Person<T> {

/** @gqlType */
class User implements Person<string> {
__typename = "User";
__typename = "User" as const;
/** @gqlField */
name: string;

Expand Down

0 comments on commit 1eed523

Please sign in to comment.