From 7ce877d98c2aa50112a41db0ed35a845f9830b5e Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Sun, 24 May 2026 13:36:21 +0200 Subject: [PATCH 1/4] feat(DEP0153): add dns lookup options codemod --- package-lock.json | 15 ++ recipes/dns-lookup-options-coercion/README.md | 25 +++ .../dns-lookup-options-coercion/codemod.yaml | 24 +++ .../dns-lookup-options-coercion/package.json | 24 +++ .../src/workflow.ts | 153 ++++++++++++++++++ .../tests/commonjs-destructured/expected.js | 3 + .../tests/commonjs-destructured/input.js | 3 + .../tests/commonjs-namespace/expected.js | 3 + .../tests/commonjs-namespace/input.js | 3 + .../tests/dns-promises-import/expected.mjs | 3 + .../tests/dns-promises-import/input.mjs | 3 + .../tests/leave-dynamic-options/expected.js | 3 + .../tests/leave-dynamic-options/input.js | 3 + .../tests/promises-alias/expected.mjs | 3 + .../tests/promises-alias/input.mjs | 3 + .../dns-lookup-options-coercion/workflow.yaml | 25 +++ 16 files changed, 296 insertions(+) create mode 100644 recipes/dns-lookup-options-coercion/README.md create mode 100644 recipes/dns-lookup-options-coercion/codemod.yaml create mode 100644 recipes/dns-lookup-options-coercion/package.json create mode 100644 recipes/dns-lookup-options-coercion/src/workflow.ts create mode 100644 recipes/dns-lookup-options-coercion/tests/commonjs-destructured/expected.js create mode 100644 recipes/dns-lookup-options-coercion/tests/commonjs-destructured/input.js create mode 100644 recipes/dns-lookup-options-coercion/tests/commonjs-namespace/expected.js create mode 100644 recipes/dns-lookup-options-coercion/tests/commonjs-namespace/input.js create mode 100644 recipes/dns-lookup-options-coercion/tests/dns-promises-import/expected.mjs create mode 100644 recipes/dns-lookup-options-coercion/tests/dns-promises-import/input.mjs create mode 100644 recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/expected.js create mode 100644 recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/input.js create mode 100644 recipes/dns-lookup-options-coercion/tests/promises-alias/expected.mjs create mode 100644 recipes/dns-lookup-options-coercion/tests/promises-alias/input.mjs create mode 100644 recipes/dns-lookup-options-coercion/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 90f7b296..3f330a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -448,6 +448,10 @@ "resolved": "recipes/dirent-path-to-parent-path", "link": true }, + "node_modules/@nodejs/dns-lookup-options-coercion": { + "resolved": "recipes/dns-lookup-options-coercion", + "link": true + }, "node_modules/@nodejs/err-invalid-callback": { "resolved": "recipes/err-invalid-callback", "link": true @@ -817,6 +821,17 @@ "@codemod.com/jssg-types": "^1.6.0" } }, + "recipes/dns-lookup-options-coercion": { + "name": "@nodejs/dns-lookup-options-coercion", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.0" + } + }, "recipes/err-invalid-callback": { "name": "@nodejs/err-invalid-callback", "version": "1.0.0", diff --git a/recipes/dns-lookup-options-coercion/README.md b/recipes/dns-lookup-options-coercion/README.md new file mode 100644 index 00000000..415cb7f3 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/README.md @@ -0,0 +1,25 @@ +# dns.lookup options type coercion DEP0153 + +Handle DEP0153 by converting literal `dns.lookup()` and `dnsPromises.lookup()` option values to their proper types. + +See [DEP0153](https://nodejs.org/api/deprecations.html#dep0153-dnslookup-and-dnspromiseslookup-options-type-coercion). + +## Example + +```diff + const dns = require("node:dns"); + +- dns.lookup("example.com", { family: "4", all: 1 }, callback); ++ dns.lookup("example.com", { family: 4, all: true }, callback); +``` + +```diff + import { lookup } from "node:dns/promises"; + +- await lookup("example.com", { family: "6", verbatim: 0 }); ++ await lookup("example.com", { family: 6, verbatim: false }); +``` + +## Limitations + +This recipe only changes literal option values. Dynamic values such as `{ family: familyOption }` need manual review. diff --git a/recipes/dns-lookup-options-coercion/codemod.yaml b/recipes/dns-lookup-options-coercion/codemod.yaml new file mode 100644 index 00000000..a6f1ec3f --- /dev/null +++ b/recipes/dns-lookup-options-coercion/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: "@nodejs/dns-lookup-options-coercion" +version: "1.0.0" +description: Handle DEP0153 by converting literal dns.lookup option values to their proper types. +author: Herrtian +license: MIT +workflow: workflow.yaml +category: migration +repository: https://github.com/nodejs/userland-migrations + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - nodejs + - dns + +registry: + access: public + visibility: public diff --git a/recipes/dns-lookup-options-coercion/package.json b/recipes/dns-lookup-options-coercion/package.json new file mode 100644 index 00000000..6490c125 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/dns-lookup-options-coercion", + "version": "1.0.0", + "description": "Handle DEP0153 by converting literal dns.lookup option values to their proper types", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/dns-lookup-options-coercion", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Herrtian", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/dns-lookup-options-coercion/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.0" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/dns-lookup-options-coercion/src/workflow.ts b/recipes/dns-lookup-options-coercion/src/workflow.ts new file mode 100644 index 00000000..c3853ccf --- /dev/null +++ b/recipes/dns-lookup-options-coercion/src/workflow.ts @@ -0,0 +1,153 @@ +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const NUMERIC_OPTIONS = new Set(['family', 'hints']); +const BOOLEAN_OPTIONS = new Set(['all', 'verbatim']); +const IGNORED_ARGUMENT_KINDS = new Set([',', '(', ')']); + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const lookupCallees = collectLookupCallees(root); + + if (lookupCallees.size === 0) return null; + + for (const callee of lookupCallees) { + const calls = rootNode.findAll({ + rule: { pattern: `${callee}($$$_ARGS)` }, + }); + + for (const call of calls) { + edits.push(...transformLookupCall(call)); + } + } + + if (edits.length === 0) return null; + + return rootNode.commitEdits(edits); +} + +function collectLookupCallees(root: SgRoot): Set { + const callees = new Set(); + + for (const statement of getModuleDependencies(root, 'dns')) { + addResolvedBinding(callees, statement, '$.lookup'); + addResolvedBinding(callees, statement, '$.promises.lookup'); + } + + for (const statement of getModuleDependencies(root, 'dns/promises')) { + addResolvedBinding(callees, statement, '$.lookup'); + } + + return callees; +} + +function addResolvedBinding( + callees: Set, + statement: SgNode, + path: string, +): void { + const binding = resolveBindingPath(statement, path); + + if (binding) { + callees.add(binding); + } +} + +function transformLookupCall(call: SgNode): Edit[] { + const args = call.field('arguments'); + if (!args) return []; + + const optionArg = args + .children() + .filter((child) => !IGNORED_ARGUMENT_KINDS.has(child.kind()))[1]; + + if (!optionArg || optionArg.kind() !== 'object') return []; + + return transformOptionsObject(optionArg); +} + +function transformOptionsObject(options: SgNode): Edit[] { + const edits: Edit[] = []; + const pairs = options.children().filter((child) => child.kind() === 'pair'); + + for (const pair of pairs) { + const key = getOptionKey(pair); + const value = pair.field('value'); + + if (!key || !value) continue; + + const replacement = getValueReplacement(key, value); + + if (replacement !== null) { + edits.push(value.replace(replacement)); + } + } + + return edits; +} + +function getOptionKey(pair: SgNode): string | null { + const key = pair.field('key'); + if (!key) return null; + + if (key.kind() === 'property_identifier') { + return key.text(); + } + + if (key.kind() === 'string') { + return getStringLiteralValue(key); + } + + return null; +} + +function getValueReplacement(key: string, value: SgNode): string | null { + if (NUMERIC_OPTIONS.has(key)) { + return getNumericReplacement(value); + } + + if (BOOLEAN_OPTIONS.has(key)) { + return getBooleanReplacement(value); + } + + return null; +} + +function getNumericReplacement(value: SgNode): string | null { + if (value.kind() !== 'string') return null; + + const stringValue = getStringLiteralValue(value); + if (!stringValue || !/^\d+$/.test(stringValue)) return null; + + return String(Number.parseInt(stringValue, 10)); +} + +function getBooleanReplacement(value: SgNode): string | null { + if (value.kind() === 'number') { + if (value.text() === '0') return 'false'; + if (value.text() === '1') return 'true'; + } + + if (value.kind() !== 'string') return null; + + const stringValue = getStringLiteralValue(value); + if (stringValue === 'true') return 'true'; + if (stringValue === 'false') return 'false'; + + return null; +} + +function getStringLiteralValue(node: SgNode): string | null { + const stringFragment = node.find({ rule: { kind: 'string_fragment' } }); + + if (stringFragment) { + return stringFragment.text(); + } + + const text = node.text(); + + return text.length >= 2 ? text.slice(1, -1) : null; +} diff --git a/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/expected.js b/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/expected.js new file mode 100644 index 00000000..f58c7c55 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/expected.js @@ -0,0 +1,3 @@ +const { lookup } = require("node:dns"); + +lookup("example.com", { family: 4, all: false }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/input.js b/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/input.js new file mode 100644 index 00000000..bec62075 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/commonjs-destructured/input.js @@ -0,0 +1,3 @@ +const { lookup } = require("node:dns"); + +lookup("example.com", { family: "4", all: "false" }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/expected.js b/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/expected.js new file mode 100644 index 00000000..4983e872 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/expected.js @@ -0,0 +1,3 @@ +const dns = require("node:dns"); + +dns.lookup("example.com", { family: 4, hints: 0, all: true, verbatim: false }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/input.js b/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/input.js new file mode 100644 index 00000000..8d28754c --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/commonjs-namespace/input.js @@ -0,0 +1,3 @@ +const dns = require("node:dns"); + +dns.lookup("example.com", { family: "4", hints: "0", all: 1, verbatim: 0 }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/dns-promises-import/expected.mjs b/recipes/dns-lookup-options-coercion/tests/dns-promises-import/expected.mjs new file mode 100644 index 00000000..f1edc75b --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/dns-promises-import/expected.mjs @@ -0,0 +1,3 @@ +import { lookup } from "node:dns/promises"; + +await lookup("example.com", { family: 6, all: true }); diff --git a/recipes/dns-lookup-options-coercion/tests/dns-promises-import/input.mjs b/recipes/dns-lookup-options-coercion/tests/dns-promises-import/input.mjs new file mode 100644 index 00000000..f6dd9ba5 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/dns-promises-import/input.mjs @@ -0,0 +1,3 @@ +import { lookup } from "node:dns/promises"; + +await lookup("example.com", { family: "6", all: "true" }); diff --git a/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/expected.js b/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/expected.js new file mode 100644 index 00000000..c91489c8 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/expected.js @@ -0,0 +1,3 @@ +const dns = require("node:dns"); + +dns.lookup("example.com", { family: familyOption, hints: dns.ADDRCONFIG, all: shouldReturnAll }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/input.js b/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/input.js new file mode 100644 index 00000000..c91489c8 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/leave-dynamic-options/input.js @@ -0,0 +1,3 @@ +const dns = require("node:dns"); + +dns.lookup("example.com", { family: familyOption, hints: dns.ADDRCONFIG, all: shouldReturnAll }, callback); diff --git a/recipes/dns-lookup-options-coercion/tests/promises-alias/expected.mjs b/recipes/dns-lookup-options-coercion/tests/promises-alias/expected.mjs new file mode 100644 index 00000000..bc284a39 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/promises-alias/expected.mjs @@ -0,0 +1,3 @@ +import { promises as dnsPromises } from "node:dns"; + +await dnsPromises.lookup("example.com", { family: 4, verbatim: false }); diff --git a/recipes/dns-lookup-options-coercion/tests/promises-alias/input.mjs b/recipes/dns-lookup-options-coercion/tests/promises-alias/input.mjs new file mode 100644 index 00000000..a5fe946a --- /dev/null +++ b/recipes/dns-lookup-options-coercion/tests/promises-alias/input.mjs @@ -0,0 +1,3 @@ +import { promises as dnsPromises } from "node:dns"; + +await dnsPromises.lookup("example.com", { family: "4", verbatim: "false" }); diff --git a/recipes/dns-lookup-options-coercion/workflow.yaml b/recipes/dns-lookup-options-coercion/workflow.yaml new file mode 100644 index 00000000..86499726 --- /dev/null +++ b/recipes/dns-lookup-options-coercion/workflow.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Handle DEP0153 by converting literal dns.lookup option values to their proper types. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From e7259af79a0409183503611f3d57fb90f9ca1ecc Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Sun, 24 May 2026 19:21:57 +0200 Subject: [PATCH 2/4] docs: explain dns lookup codemod scope --- .../src/workflow.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/recipes/dns-lookup-options-coercion/src/workflow.ts b/recipes/dns-lookup-options-coercion/src/workflow.ts index c3853ccf..ccca5ff5 100644 --- a/recipes/dns-lookup-options-coercion/src/workflow.ts +++ b/recipes/dns-lookup-options-coercion/src/workflow.ts @@ -56,6 +56,12 @@ function addResolvedBinding( } } +/** + * Rewrites the second argument only when it is an inline options object. + * + * DEP0153 affects option values, so calls without options or with a shared + * options variable are intentionally left unchanged for manual review. + */ function transformLookupCall(call: SgNode): Edit[] { const args = call.field('arguments'); if (!args) return []; @@ -69,6 +75,11 @@ function transformLookupCall(call: SgNode): Edit[] { return transformOptionsObject(optionArg); } +/** + * Converts known dns.lookup option keys when the value can be replaced without + * changing surrounding code. Other keys are ignored so this recipe stays scoped + * to the DEP0153 runtime coercions. + */ function transformOptionsObject(options: SgNode): Edit[] { const edits: Edit[] = []; const pairs = options.children().filter((child) => child.kind() === 'pair'); @@ -104,6 +115,13 @@ function getOptionKey(pair: SgNode): string | null { return null; } +/** + * Returns a replacement for deprecated literal coercions only. + * + * Semantic propagation for identifiers is deliberately avoided here: options + * objects can be reused, mutated, or passed through helpers, and an unsafe + * rewrite would be harder to review than leaving those cases for the user. + */ function getValueReplacement(key: string, value: SgNode): string | null { if (NUMERIC_OPTIONS.has(key)) { return getNumericReplacement(value); From 6b9e5ce0870df49642cd68cc07e20ac61037d5e3 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Wed, 27 May 2026 16:27:14 +0200 Subject: [PATCH 3/4] Address dns lookup review --- .../src/workflow.ts | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/recipes/dns-lookup-options-coercion/src/workflow.ts b/recipes/dns-lookup-options-coercion/src/workflow.ts index ccca5ff5..f2ece0f0 100644 --- a/recipes/dns-lookup-options-coercion/src/workflow.ts +++ b/recipes/dns-lookup-options-coercion/src/workflow.ts @@ -12,19 +12,26 @@ export default function transform(root: SgRoot): string | null { const edits: Edit[] = []; const lookupCallees = collectLookupCallees(root); - if (lookupCallees.size === 0) return null; + if (!lookupCallees.size) return null; for (const callee of lookupCallees) { - const calls = rootNode.findAll({ - rule: { pattern: `${callee}($$$_ARGS)` }, + const calls = rootNode.findAll<'call_expression'>({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: callee.includes('.') ? 'member_expression' : 'identifier', + pattern: callee, + }, + }, }); for (const call of calls) { - edits.push(...transformLookupCall(call)); + processLookupCall(call, edits); } } - if (edits.length === 0) return null; + if (!edits.length) return null; return rootNode.commitEdits(edits); } @@ -62,17 +69,17 @@ function addResolvedBinding( * DEP0153 affects option values, so calls without options or with a shared * options variable are intentionally left unchanged for manual review. */ -function transformLookupCall(call: SgNode): Edit[] { +function processLookupCall(call: SgNode, edits: Edit[]): void { const args = call.field('arguments'); - if (!args) return []; + if (!args) return; const optionArg = args .children() .filter((child) => !IGNORED_ARGUMENT_KINDS.has(child.kind()))[1]; - if (!optionArg || optionArg.kind() !== 'object') return []; + if (!optionArg || optionArg.kind() !== 'object') return; - return transformOptionsObject(optionArg); + edits.push(...transformOptionsObject(optionArg)); } /** @@ -82,7 +89,7 @@ function transformLookupCall(call: SgNode): Edit[] { */ function transformOptionsObject(options: SgNode): Edit[] { const edits: Edit[] = []; - const pairs = options.children().filter((child) => child.kind() === 'pair'); + const pairs = options.findAll<'pair'>({ rule: { kind: 'pair' } }); for (const pair of pairs) { const key = getOptionKey(pair); @@ -100,7 +107,7 @@ function transformOptionsObject(options: SgNode): Edit[] { return edits; } -function getOptionKey(pair: SgNode): string | null { +function getOptionKey(pair: SgNode): string | null { const key = pair.field('key'); if (!key) return null; @@ -135,27 +142,37 @@ function getValueReplacement(key: string, value: SgNode): string | null { } function getNumericReplacement(value: SgNode): string | null { - if (value.kind() !== 'string') return null; + const valueKind = value.kind(); - const stringValue = getStringLiteralValue(value); - if (!stringValue || !/^\d+$/.test(stringValue)) return null; + switch (valueKind) { + case 'string': { + const stringValue = getStringLiteralValue(value); + if (!stringValue || !/^\d+$/.test(stringValue)) return null; - return String(Number.parseInt(stringValue, 10)); + return String(Number.parseInt(stringValue, 10)); + } + default: + return null; + } } function getBooleanReplacement(value: SgNode): string | null { - if (value.kind() === 'number') { - if (value.text() === '0') return 'false'; - if (value.text() === '1') return 'true'; + const valueKind = value.kind(); + + switch (valueKind) { + case 'number': + if (value.text() === '0') return 'false'; + if (value.text() === '1') return 'true'; + return null; + case 'string': { + const stringValue = getStringLiteralValue(value); + return stringValue === 'true' || stringValue === 'false' + ? stringValue + : null; + } + default: + return null; } - - if (value.kind() !== 'string') return null; - - const stringValue = getStringLiteralValue(value); - if (stringValue === 'true') return 'true'; - if (stringValue === 'false') return 'false'; - - return null; } function getStringLiteralValue(node: SgNode): string | null { From b6b47d6616f9f676c1a4193c56396c241772e6b0 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Thu, 28 May 2026 21:24:18 +0200 Subject: [PATCH 4/4] Address dns lookup review comments Signed-off-by: Herrtian <70463940+Herrtian@users.noreply.github.com> --- recipes/dns-lookup-options-coercion/src/workflow.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/recipes/dns-lookup-options-coercion/src/workflow.ts b/recipes/dns-lookup-options-coercion/src/workflow.ts index f2ece0f0..0061a048 100644 --- a/recipes/dns-lookup-options-coercion/src/workflow.ts +++ b/recipes/dns-lookup-options-coercion/src/workflow.ts @@ -5,7 +5,6 @@ import type Js from '@codemod.com/jssg-types/langs/javascript'; const NUMERIC_OPTIONS = new Set(['family', 'hints']); const BOOLEAN_OPTIONS = new Set(['all', 'verbatim']); -const IGNORED_ARGUMENT_KINDS = new Set([',', '(', ')']); export default function transform(root: SgRoot): string | null { const rootNode = root.root(); @@ -73,9 +72,7 @@ function processLookupCall(call: SgNode, edits: Edit[]): void { const args = call.field('arguments'); if (!args) return; - const optionArg = args - .children() - .filter((child) => !IGNORED_ARGUMENT_KINDS.has(child.kind()))[1]; + const optionArg = args.children().filter((child) => child.isNamed())[1]; if (!optionArg || optionArg.kind() !== 'object') return; @@ -111,11 +108,12 @@ function getOptionKey(pair: SgNode): string | null { const key = pair.field('key'); if (!key) return null; - if (key.kind() === 'property_identifier') { + const keyKind = key.kind(); + if (keyKind === 'property_identifier') { return key.text(); } - if (key.kind() === 'string') { + if (keyKind === 'string') { return getStringLiteralValue(key); }