Skip to content

Commit

Permalink
feat: add qs codemod
Browse files Browse the repository at this point in the history
Adds a codemod for migrating from `qs` to `picoquery`.

Some options qs offers are not available. Currently, this mod will not
warn on those but should in future.
  • Loading branch information
43081j committed Aug 6, 2024
1 parent d98052f commit 944d9fc
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 0 deletions.
167 changes: 167 additions & 0 deletions codemods/qs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ts } from '@ast-grep/napi';

const qsLikeOptions = {
nesting: true,
nestingSyntax: 'js',
arrayRepeat: true,
arrayRepeatSyntax: 'bracket',
};
const qsLikeOptionsStr = JSON.stringify(qsLikeOptions);

/**
* @typedef {import('../../types.js').Codemod} Codemod
* @typedef {import('../../types.js').CodemodOptions} CodemodOptions
* @typedef {import('@ast-grep/napi').SgNode} SgNode
*/

/**
* @param {SgNode} obj
*/
function parseOptions(obj) {
/** @type {Record<string, SgNode>} */
const result = {};

for (const child of obj.children()) {
const key = child.field('key');
const val = child.field('value');

if (key && val) {
result[key.text()] = val;
}
}

return result;
}

/**
* @param {CodemodOptions} [options]
* @returns {Codemod}
*/
export default function (options) {
return {
name: 'qs',
transform: ({ file }) => {
const ast = ts.parse(file.source);
const root = ast.root();
const imports = root.findAll({
rule: {
pattern: {
context: "import $NAME from 'qs'",
strictness: 'relaxed',
},
},
});
const requires = root.findAll({
rule: {
pattern: {
context: "require('qs')",
strictness: 'relaxed',
},
},
});
let importName = 'qs';
const edits = [];

for (const imp of imports) {
const source = imp.field('source');

if (!source) {
continue;
}

const quoteType = source.text().startsWith("'") ? "'" : '"';
const nameMatch = imp.getMatch('NAME');

if (nameMatch) {
importName = nameMatch.text();
edits.push(nameMatch.replace('pq'));
}

edits.push(source.replace(`${quoteType}picoquery${quoteType}`));
}

for (const req of requires) {
const args = req.field('arguments');
const firstArg = args?.child(1);
const quoteType = firstArg?.text().startsWith('"') ? '"' : "'";

edits.push(req.replace(`require(${quoteType}picoquery${quoteType})`));

const parent = req.parent();

if (parent && parent.kind() === 'variable_declarator') {
const name = parent.field('name');
if (name) {
importName = name.text();
edits.push(name.replace('pq'));
}
}
}

const expressions = root.findAll({
rule: {
pattern: `${importName}.$METHOD($$$ARGS)`,
},
});

for (const expr of expressions) {
const method = expr.getMatch('METHOD');
const args = expr.getMultipleMatches('ARGS');
const methodText = method?.text();
const func = expr.field('function')?.field('object');

if (
!func ||
!method ||
(methodText !== 'parse' && methodText !== 'stringify')
) {
continue;
}

edits.push(func.replace('pq'));

if (args.length === 1) {
edits.push(args[0].replace(`${args[0].text()}, ${qsLikeOptionsStr}`));
} else if (args.length > 2) {
const opts = parseOptions(args[2]);

/** @type {Record<string, unknown>} */
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;
}
}

if (opts.allowDots && opts.allowDots.kind() === 'true') {
newOptions.nestingSyntax = 'dot';
}

if (opts.parseArrays && opts.parseArrays.kind() === 'false') {
newOptions.arrayRepeat = false;
}

if (opts.delimiter && opts.delimiter.kind() === 'string') {
const delimiter = opts.delimiter.child(1)?.text();
if (delimiter) {
newOptions.delimiter = delimiter;
}
}

edits.push(args[2].replace(JSON.stringify(newOptions)));
}
}

return root.commitEdits(edits);
},
};
}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import parseint from './codemods/parseint/index.js';
import promiseAllsettled from './codemods/promise.allsettled/index.js';
import promiseAny from './codemods/promise.any/index.js';
import promisePrototypeFinally from './codemods/promise.prototype.finally/index.js';
import qs from './codemods/qs/index.js';
import reflectGetprototypeof from './codemods/reflect.getprototypeof/index.js';
import reflectOwnkeys from './codemods/reflect.ownkeys/index.js';
import regexpPrototypeFlags from './codemods/regexp.prototype.flags/index.js';
Expand Down Expand Up @@ -275,6 +276,7 @@ export const codemods = {
"promise.allsettled": promiseAllsettled,
"promise.any": promiseAny,
"promise.prototype.finally": promisePrototypeFinally,
"qs": qs,
"reflect.getprototypeof": reflectGetprototypeof,
"reflect.ownkeys": reflectOwnkeys,
"regexp.prototype.flags": regexpPrototypeFlags,
Expand Down
34 changes: 34 additions & 0 deletions test/fixtures/qs/basic/after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pq from 'picoquery';

const obj = {foo: 'bar'};
const query = 'foo=bar';

// encode: false, indices: false
pq.stringify(obj, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// defaults
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"});

// arrayFormat: repeat
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// arrayFormat: indices
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"});

// arrayFormat: brackets
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"});

// encode: false, indices: false
pq.parse(query, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// 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"});

// parseArrays: false
pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"});
34 changes: 34 additions & 0 deletions test/fixtures/qs/basic/before.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import qs from 'qs';

const obj = {foo: 'bar'};
const query = 'foo=bar';

// encode: false, indices: false
qs.stringify(obj, { encode: false, indices: false });

// defaults
qs.stringify(obj);

// arrayFormat: repeat
qs.stringify(obj, {arrayFormat: 'repeat'});

// arrayFormat: indices
qs.stringify(obj, {arrayFormat: 'indices'});

// arrayFormat: brackets
qs.stringify(obj, {arrayFormat: 'brackets'});

// encode: false, indices: false
qs.parse(query, { encode: false, indices: false });

// defaults
qs.parse(query);

// delimiter
qs.parse('a=foo;b=bar', {delimiter: ';'});

// allowDots
qs.parse('a.b=c', {allowDots: true});

// parseArrays: false
qs.parse('a[]=1&a[]=2', {parseArrays: false});
34 changes: 34 additions & 0 deletions test/fixtures/qs/basic/result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pq from 'picoquery';

const obj = {foo: 'bar'};
const query = 'foo=bar';

// encode: false, indices: false
pq.stringify(obj, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// defaults
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"});

// arrayFormat: repeat
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// arrayFormat: indices
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"});

// arrayFormat: brackets
pq.stringify(obj, {"nesting":true,"nestingSyntax":"js","arrayRepeat":true,"arrayRepeatSyntax":"bracket"});

// encode: false, indices: false
pq.parse(query, {"nesting":true,"nestingSyntax":"dot","arrayRepeat":true,"arrayRepeatSyntax":"repeat"});

// 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"});

// parseArrays: false
pq.parse('a[]=1&a[]=2', {"nesting":true,"nestingSyntax":"js","arrayRepeat":false,"arrayRepeatSyntax":"bracket"});

0 comments on commit 944d9fc

Please sign in to comment.