Repository: graphprotocol/graph-tooling
Pull request #1929 (2025-01-28 17:39:08)

`graph codegen`: handle reserved words
Fixes https://github.com/graphprotocol/graph-tooling/issues/1896

- Renames fields with reserved name in generated code "yield" -> "yield_"
- Handles all typescript/assemblyscript reserved words
- test


cloudflare-workers-and-pages[bot]: ## Deploying graph-tooling with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>5503b14</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://f63a8db5.graph-tooling.pages.dev'>https://f63a8db5.graph-tooling.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://yaro-codegen-reserved-words.graph-tooling.pages.dev'>https://yaro-codegen-reserved-words.graph-tooling.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/graph-tooling/f63a8db5-8f47-4bc9-9a9f-eb51adb23fb0)

github-actions[bot]: <!-- changesetsSnapshotPrCommentKey -->
### 🚀 Snapshot Release (`alpha`)

The latest changes of this PR are available as `alpha` on npm (based on the declared `changesets`):
| Package | Version | Info |
|------|---------|----|
| `@graphprotocol/graph-cli` | `0.95.1-alpha-20250128173939-5503b142cabd2dc2064e0b3238ee8dcadd175014` | [npm ↗︎](https://www.npmjs.com/package/@graphprotocol/graph-cli/v/0.95.1-alpha-20250128173939-5503b142cabd2dc2064e0b3238ee8dcadd175014) [unpkg ↗︎](https://unpkg.com/browse/@graphprotocol/graph-cli@0.95.1-alpha-20250128173939-5503b142cabd2dc2064e0b3238ee8dcadd175014/) |
changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: f6ea8924b365aea798fbd645bad0e8a1e539c94a

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                     | Type  |
| ------------------------ | ----- |
| @graphprotocol/graph-cli | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/graphprotocol/graph-tooling/new/yaro/codegen-reserved-words?filename=.changeset/late-dolls-juggle.md&value=---%0A%22%40graphprotocol%2Fgraph-cli%22%3A%20patch%0A---%0A%0A%60graph%20codegen%60%3A%20handle%20reserved%20words%0A)


YaroShkvorets: That would mean renaming these fields in subgraph GraphQL schema, and that's unnecessary and leaks implementation details.

Patch:
diff --git a/.changeset/giant-eyes-play.md b/.changeset/giant-eyes-play.md
new file mode 100644
index 00000000..40e55821
--- /dev/null
+++ b/.changeset/giant-eyes-play.md
@@ -0,0 +1,5 @@
+---
+'@graphprotocol/graph-cli': minor
+---
+
+`graph codegen`: handle events with fields with reserved names #1896
diff --git a/packages/cli/src/codegen/schema.ts b/packages/cli/src/codegen/schema.ts
index 2788b29d..ef98482a 100644
--- a/packages/cli/src/codegen/schema.ts
+++ b/packages/cli/src/codegen/schema.ts
@@ -11,6 +11,7 @@ import debug from '../debug.js';
 import Schema from '../schema.js';
 import * as typesCodegen from './types/index.js';
 import * as tsCodegen from './typescript.js';
+import * as util from './util.js';
 
 class IdField {
   static BYTES = Symbol('Bytes');
@@ -308,6 +309,7 @@ export default class SchemaCodeGenerator {
   _generateEntityFieldGetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
     const isDerivedField = this._isDerivedField(fieldDef);
     const name = fieldDef.name.value;
+    const safeName = util.handleReservedWord(name);
 
     if (isDerivedField) {
       schemaCodeGeneratorDebug.extend('_generateEntityFieldGetter')(
@@ -339,7 +341,7 @@ export default class SchemaCodeGenerator {
                         }`;
 
     return tsCodegen.method(
-      `get ${name}`,
+      `get ${safeName}`,
       [],
       returnType,
       `
@@ -351,6 +353,8 @@ export default class SchemaCodeGenerator {
   _generateDerivedFieldGetter(entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
     const entityName = entityDef.name.value;
     const name = fieldDef.name.value;
+    const safeName = util.handleReservedWord(name);
+
     schemaCodeGeneratorDebug.extend('_generateDerivedFieldGetter')(
       `Generating derived field '${name}' getter for Entity '${entityName}'`,
     );
@@ -390,7 +394,7 @@ export default class SchemaCodeGenerator {
     const toValueString = idIsBytes ? '.toBytes().toHexString()' : '.toString()';
 
     return tsCodegen.method(
-      `get ${name}`,
+      `get ${safeName}`,
       [],
       returnType,
       `
@@ -415,6 +419,7 @@ export default class SchemaCodeGenerator {
     fieldDef: FieldDefinitionNode,
   ) {
     const name = fieldDef.name.value;
+    const safeName = util.handleReservedWord(name);
     const gqlType = fieldDef.type;
     const fieldValueType = this._valueTypeFromGraphQl(gqlType);
     const returnType = this._typeFromGraphQl(gqlType);
@@ -428,7 +433,7 @@ export default class SchemaCodeGenerator {
                         }`;
 
     return tsCodegen.method(
-      `get ${name}`,
+      `get ${safeName}`,
       [],
       returnType,
       `
@@ -439,6 +444,7 @@ export default class SchemaCodeGenerator {
   }
   _generateEntityFieldSetter(_entityDef: ObjectTypeDefinitionNode, fieldDef: FieldDefinitionNode) {
     const name = fieldDef.name.value;
+    const safeName = util.handleReservedWord(name);
     const isDerivedField = !!fieldDef.directives?.find(
       directive => directive.name.value === 'derivedFrom',
     );
@@ -477,7 +483,7 @@ Suggestion: add an '!' to the member type of the List, change from '[${baseType}
     `;
 
     return tsCodegen.method(
-      `set ${name}`,
+      `set ${safeName}`,
       [tsCodegen.param('value', paramType)],
       undefined,
       isNullable ? setNullable : setNonNullable,
diff --git a/packages/cli/src/codegen/util.ts b/packages/cli/src/codegen/util.ts
index c675ad24..29a72c30 100644
--- a/packages/cli/src/codegen/util.ts
+++ b/packages/cli/src/codegen/util.ts
@@ -9,7 +9,7 @@ export function disambiguateNames<T>({
 }) {
   const collisionCounter = new Map();
   return values.map((value, index) => {
-    const name = getName(value, index);
+    const name = handleReservedWord(getName(value, index));
     const counter = collisionCounter.get(name);
     if (counter === undefined) {
       collisionCounter.set(name, 1);
@@ -20,6 +20,54 @@ export function disambiguateNames<T>({
   });
 }
 
+const RESERVED_WORDS = new Set([
+  'await',
+  'break',
+  'case',
+  'catch',
+  'class',
+  'const',
+  'continue',
+  'debugger',
+  'delete',
+  'do',
+  'else',
+  'enum',
+  'export',
+  'extends',
+  'false',
+  'finally',
+  'function',
+  'if',
+  'implements',
+  'import',
+  'in',
+  'interface',
+  'let',
+  'new',
+  'package',
+  'private',
+  'protected',
+  'public',
+  'return',
+  'super',
+  'switch',
+  'static',
+  'this',
+  'throw',
+  'true',
+  'try',
+  'typeof',
+  'var',
+  'while',
+  'with',
+  'yield',
+]);
+
+export function handleReservedWord(name: string): string {
+  return RESERVED_WORDS.has(name) ? `${name}_` : name;
+}
+
 export function isTupleType(t: string) {
   return t === 'tuple';
 }
diff --git a/packages/cli/src/scaffold/mapping.ts b/packages/cli/src/scaffold/mapping.ts
index 818f967b..a363f21c 100644
--- a/packages/cli/src/scaffold/mapping.ts
+++ b/packages/cli/src/scaffold/mapping.ts
@@ -13,7 +13,10 @@ export const generateFieldAssignment = (
   value: string[],
   type: string,
 ): { assignment: string; imports: string[] } => {
-  let rightSide = `event.params.${value.join('.')}`;
+  const safeKey = key.map(k => util.handleReservedWord(k));
+  const safeValue = value.map(v => util.handleReservedWord(v));
+
+  let rightSide = `event.params.${safeValue.join('.')}`;
   const imports = [];
 
   if (type in VALUE_TYPECAST_MAP) {
@@ -23,7 +26,7 @@ export const generateFieldAssignment = (
   }
 
   return {
-    assignment: `entity.${key.join('_')} = ${rightSide}`,
+    assignment: `entity.${safeKey.join('_')} = ${rightSide}`,
     imports,
   };
 };


Test patch:
diff --git a/packages/cli/src/codegen/schema.test.ts b/packages/cli/src/codegen/schema.test.ts
index 6f549d01..cbf3002c 100644
--- a/packages/cli/src/codegen/schema.test.ts
+++ b/packages/cli/src/codegen/schema.test.ts
@@ -72,6 +72,9 @@ describe('Schema code generator', { concurrent: true }, () => {
         count: Int!
         isActive: Boolean
 
+        # reserved word
+        yield: Int!
+
         # derivedFrom
         wallets: [Wallet!] @derivedFrom(field: "account")
 
@@ -248,6 +251,26 @@ describe('Schema code generator', { concurrent: true }, () => {
               this.set('count', Value.fromI32(value))
             `,
           },
+          {
+            name: 'get yield_',
+            params: [],
+            returnType: new NamedType('i32'),
+            body: `let value = this.get('yield')
+            if (!value || value.kind == ValueKind.NULL) {
+              return 0
+            } else {
+              return value.toI32()
+            }
+            `,
+          },
+          {
+            name: 'set yield_',
+            params: [new Param('value', new NamedType('i32'))],
+            returnType: undefined,
+            body: `
+              this.set('yield', Value.fromI32(value))
+            `,
+          },
           {
             name: 'get isActive',
             params: [],