From 7f38e136942d1157c90c1a0923a80824204aa462 Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:33:08 +0100 Subject: [PATCH] fix: add better handling for unknown options --- codemods/qs/index.js | 174 +++++++++++++++++++++++++++---- test/fixtures/qs/basic/after.js | 23 +++- test/fixtures/qs/basic/before.js | 25 ++++- test/fixtures/qs/basic/result.js | 23 +++- 4 files changed, 209 insertions(+), 36 deletions(-) diff --git a/codemods/qs/index.js b/codemods/qs/index.js index 0f39230..6cf8ebc 100644 --- a/codemods/qs/index.js +++ b/codemods/qs/index.js @@ -16,6 +16,7 @@ const qsLikeOptionsStr = JSON.stringify(qsLikeOptions); /** * @param {SgNode} obj + * @return {Record} */ function parseOptions(obj) { /** @type {Record} */ @@ -33,6 +34,97 @@ function parseOptions(obj) { return result; } +/** @typedef {Record | ((value: SgNode) => Record|null) | null} ReplacementOptions */ +/** @typedef {({kind: string; options: ReplacementOptions})} Replacement */ +/** @typedef {({replacements: Replacement[]})} Replacer */ +/** @type {Record} */ +const replacements = { + indices: { + replacements: [ + { + kind: 'false', + options: { + nestingSyntax: 'dot', + arrayRepeatSyntax: 'repeat', + }, + }, + { + kind: 'true', + options: { + nestingSyntax: 'js', + }, + }, + ], + }, + arrayFormat: { + replacements: [ + { + kind: 'string', + options: (value) => { + const formatStr = value.child(1)?.text(); + if (formatStr === 'repeat') { + return { arrayRepeatSyntax: 'repeat' }; + } else if (formatStr === 'indices') { + return { arrayRepeat: false }; + } + return { arrayRepeatSyntax: 'bracket' }; + }, + }, + ], + }, + allowDots: { + replacements: [ + { + kind: 'true', + options: { + nestingSyntax: 'js', + }, + }, + { + kind: 'false', + options: { + nestingSyntax: 'index', + }, + }, + ], + }, + parseArrays: { + replacements: [ + { + kind: 'false', + options: { + arrayRepeat: false, + }, + }, + { + kind: 'true', + options: { + arrayRepeat: true, + }, + }, + ], + }, + delimiter: { + replacements: [ + { + kind: 'string', + options: (value) => { + const delimiter = value.child(1)?.text(); + if (delimiter) { + return { delimiter }; + } + console.warn( + `Warning: encountered a delimiter we could not ` + + `transform. It will be dropped, so may need additional fixes ` + + `after this codemod executes`, + ); + return null; + }, + }, + ], + }, +}; + /** * @param {CodemodOptions} [options] * @returns {Codemod} @@ -118,7 +210,7 @@ export default function (options) { continue; } - edits.push(func.replace('pq')); + let decodeResult = false; if (args.length === 1) { edits.push(args[0].replace(`${args[0].text()}, ${qsLikeOptionsStr}`)); @@ -128,37 +220,73 @@ export default function (options) { /** @type {Record} */ const newOptions = { ...qsLikeOptions }; - if (opts.indices && opts.indices.kind() === 'false') { - newOptions.nestingSyntax = 'dot'; - newOptions.arrayRepeatSyntax = 'repeat'; - } - - if (opts.arrayFormat && opts.arrayFormat.kind() === 'string') { - const arrayFormat = opts.arrayFormat.child(1)?.text(); - if (arrayFormat === 'repeat') { - newOptions.arrayRepeatSyntax = 'repeat'; - } else if (arrayFormat === 'indices') { - newOptions.arrayRepeat = false; + for (const [key, val] of Object.entries(opts)) { + // Special case for the `encode` option if it is `false`, as we + // need to wrap the entire result with `decodeURIComponent` + if (key === 'encode' && val.kind() === 'false') { + decodeResult = true; + continue; } - } - if (opts.allowDots && opts.allowDots.kind() === 'true') { - newOptions.nestingSyntax = 'dot'; - } + const replacer = replacements[key]; - if (opts.parseArrays && opts.parseArrays.kind() === 'false') { - newOptions.arrayRepeat = false; - } + if (!replacer) { + console.warn( + `Warning: encountered an unknown option. ` + + `The option ("${key}") will be dropped, so may need ` + + `additional fixes after this codemod executes.`, + ); + continue; + } - if (opts.delimiter && opts.delimiter.kind() === 'string') { - const delimiter = opts.delimiter.child(1)?.text(); - if (delimiter) { - newOptions.delimiter = delimiter; + let foundReplacement = false; + for (const replacement of replacer.replacements) { + if (replacement.kind === val.kind()) { + const replacementOpts = + typeof replacement.options === 'function' + ? replacement.options(val) + : replacement.options; + foundReplacement = true; + if (replacementOpts) { + for (const optKey in replacementOpts) { + newOptions[optKey] = replacementOpts[optKey]; + } + } + } + } + + if (!foundReplacement) { + console.warn( + `Warning: encountered an option with a value we could not parse. ` + + `The option ("${key}") has a computed value or an unexpected ` + + `type. It will be dropped, so may need additional fixes ` + + `after this codemod executes.`, + ); } } edits.push(args[2].replace(JSON.stringify(newOptions))); } + + if (decodeResult) { + console.warn( + `Warning: the "encode: false" option will be ` + + `replaced by a call to decodeURIComponent`, + ); + edits.push(func.replace('decodeURIComponent(pq')); + + const argsChildren = expr.field('arguments')?.children(); + + if (argsChildren) { + const lastArgsChild = argsChildren[argsChildren.length - 1]; + + if (lastArgsChild.kind() === ')') { + edits.push(lastArgsChild.replace(`${lastArgsChild.text()})`)); + } + } + } else { + edits.push(func.replace('pq')); + } } return root.commitEdits(edits); diff --git a/test/fixtures/qs/basic/after.js b/test/fixtures/qs/basic/after.js index 44544b5..8832aff 100644 --- a/test/fixtures/qs/basic/after.js +++ b/test/fixtures/qs/basic/after.js @@ -3,7 +3,7 @@ import pq from 'picoquery'; const obj = {foo: 'bar'}; const query = 'foo=bar'; -// encode: false, indices: false +// indices: false pq.stringify(obj, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"}); // defaults @@ -18,17 +18,32 @@ pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arra // arrayFormat: brackets pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); -// encode: false, indices: false +// arrayFormat: nonsense defaults to bracket +pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + +// encode: false +decodeURIComponent(pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"})); + +// indices: false pq.parse(query, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"}); +// indices: true +pq.parse(query, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + // defaults pq.parse(query, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); // delimiter pq.parse('a=foo;b=bar', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket","delimiter":";"}); -// allowDots -pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); +// allowDots: true +pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + +// allowDots: false +pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"index","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); // parseArrays: false pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"}); + +// parseArrays: true +pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); diff --git a/test/fixtures/qs/basic/before.js b/test/fixtures/qs/basic/before.js index 573fdfe..8bcfddd 100644 --- a/test/fixtures/qs/basic/before.js +++ b/test/fixtures/qs/basic/before.js @@ -3,8 +3,8 @@ import qs from 'qs'; const obj = {foo: 'bar'}; const query = 'foo=bar'; -// encode: false, indices: false -qs.stringify(obj, { encode: false, indices: false }); +// indices: false +qs.stringify(obj, {indices: false}); // defaults qs.stringify(obj); @@ -18,8 +18,17 @@ qs.stringify(obj, {arrayFormat: 'indices'}); // arrayFormat: brackets qs.stringify(obj, {arrayFormat: 'brackets'}); -// encode: false, indices: false -qs.parse(query, { encode: false, indices: false }); +// arrayFormat: nonsense defaults to bracket +qs.stringify(obj, {arrayFormat: 'absolute gibberish'}); + +// encode: false +qs.stringify(obj, {encode: false}); + +// indices: false +qs.parse(query, { indices: false }); + +// indices: true +qs.parse(query, { indices: true }); // defaults qs.parse(query); @@ -27,8 +36,14 @@ qs.parse(query); // delimiter qs.parse('a=foo;b=bar', {delimiter: ';'}); -// allowDots +// allowDots: true qs.parse('a.b=c', {allowDots: true}); +// allowDots: false +qs.parse('a.b=c', {allowDots: false}); + // parseArrays: false qs.parse('a[]=1&a[]=2', {parseArrays: false}); + +// parseArrays: true +qs.parse('a[]=1&a[]=2', {parseArrays: true}); diff --git a/test/fixtures/qs/basic/result.js b/test/fixtures/qs/basic/result.js index 44544b5..8832aff 100644 --- a/test/fixtures/qs/basic/result.js +++ b/test/fixtures/qs/basic/result.js @@ -3,7 +3,7 @@ import pq from 'picoquery'; const obj = {foo: 'bar'}; const query = 'foo=bar'; -// encode: false, indices: false +// indices: false pq.stringify(obj, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"}); // defaults @@ -18,17 +18,32 @@ pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arra // arrayFormat: brackets pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); -// encode: false, indices: false +// arrayFormat: nonsense defaults to bracket +pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + +// encode: false +decodeURIComponent(pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"})); + +// indices: false pq.parse(query, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"}); +// indices: true +pq.parse(query, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + // defaults pq.parse(query, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); // delimiter pq.parse('a=foo;b=bar', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket","delimiter":";"}); -// allowDots -pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); +// allowDots: true +pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); + +// allowDots: false +pq.parse('a.b=c', {"nesting":true,"nestingSyntax":"index","arrayRepeat":true,"arrayRepeatSyntax":"bracket"}); // parseArrays: false pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"}); + +// parseArrays: true +pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"});