diff --git a/README.md b/README.md index d6bec49..7bb89de 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ async function foo(msg: T): void { } ``` -One more example that `/// to-promis-all` converts a sequence of `await` expressions to `await Promise.all()`: +One more example that `/// to-promise-all` converts a sequence of `await` expressions to `await Promise.all()`: diff --git a/src/commands/to-one-line.md b/src/commands/to-one-line.md new file mode 100644 index 0000000..02ac078 --- /dev/null +++ b/src/commands/to-one-line.md @@ -0,0 +1,25 @@ +# `to-one-line` + +Convert multiple lines object to one line object. + +## Triggers + +- `/// to-one-line` +- `/// tol` +- `/// 21l` + +## Examples + +```js +/// to-one-line +const foo = { + bar: 1, + baz: 2 +} +``` + +Will be converted to: + +```js +const foo = { bar: 1, baz: 2 } +``` diff --git a/src/commands/to-one-line.test.ts b/src/commands/to-one-line.test.ts new file mode 100644 index 0000000..59dd3b3 --- /dev/null +++ b/src/commands/to-one-line.test.ts @@ -0,0 +1,146 @@ +import { $, run } from './_test-utils' +import { toOneLine as command } from './to-one-line' + +run( + command, + { + code: $` + /// to-one-line + const foo = { + bar: 1, + baz: 2, + } + `, + output: $` + const foo = { bar: 1, baz: 2 } + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + const arr = [ + 1, + 2, + 3, + 4, + ] + `, + output: $` + const arr = [1, 2, 3, 4] + `, + errors: ['command-fix'], + }, + { + code: $` + /// tol + obj = { + x: 100, + y: 200, + } + `, + output: $` + obj = { x: 100, y: 200 } + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + const data = { + user: { + name: 'Alice', + age: 30, + }, + scores: [ + 10, + 20, + 30, + ], + } + `, + output: $` + const data = { user: { name: 'Alice', age: 30 }, scores: [10, 20, 30] } + `, + errors: ['command-fix'], + }, + { + code: $` + /// 21l + const alreadyOneLine = { a: 1, b: 2 } + `, + output: $` + const alreadyOneLine = { a: 1, b: 2 } + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + const fruits = [ + "apple", + "banana", + "cherry", + ] + `, + output: $` + const fruits = ["apple", "banana", "cherry"] + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + whichFruitIsTheBest([ + "apple", + "banana", + "cherry", + ]) + `, + output: $` + whichFruitIsTheBest(["apple", "banana", "cherry"]) + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + function whichFruitIsTheBest({ + apple, + banana, + cherry, + }) {} + `, + output: $` + function whichFruitIsTheBest({ apple, banana, cherry }) {} + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + function f([ + a, + b, + c, + ]) {} + `, + output: $` + function f([a, b, c]) {} + `, + errors: ['command-fix'], + }, + { + code: $` + /// to-one-line + return { + foo: 1, + bar: 2, + } + `, + output: $` + return { foo: 1, bar: 2 } + `, + errors: ['command-fix'], + }, +) diff --git a/src/commands/to-one-line.ts b/src/commands/to-one-line.ts new file mode 100644 index 0000000..5a1ce83 --- /dev/null +++ b/src/commands/to-one-line.ts @@ -0,0 +1,87 @@ +import type { Command, NodeType, Tree } from '../types' + +export const toOneLine: Command = { + name: 'to-one-line', + match: /^[/@:]\s*(?:to-one-line|21l|tol)$/, + action(ctx) { + const node = ctx.findNodeBelow( + 'VariableDeclaration', + 'AssignmentExpression', + 'CallExpression', + 'FunctionDeclaration', + 'FunctionExpression', + 'ReturnStatement', + ) + if (!node) + return ctx.reportError('Unable to find node to convert') + + let target: Tree.Node | null = null + + // For a variable declaration we use the initializer. + if (node.type === 'VariableDeclaration') { + const decl = node.declarations[0] + if (decl && decl.init && isAllowedType(decl.init.type)) + target = decl.init + } + // For an assignment we use the right side. + else if (node.type === 'AssignmentExpression') { + if (node.right && isAllowedType(node.right.type)) + target = node.right + } + // In a call we search the arguments. + else if (node.type === 'CallExpression') { + target = node.arguments.find(arg => isAllowedType(arg.type)) || null + } + // In a function we search the parameters. + else if ( + node.type === 'FunctionDeclaration' + || node.type === 'FunctionExpression' + ) { + target = node.params.find(param => isAllowedType(param.type)) || null + } + // For a return statement we use its argument. + else if (node.type === 'ReturnStatement') { + if (node.argument && isAllowedType(node.argument.type)) + target = node.argument + } + + if (!target) + return ctx.reportError('Unable to find object/array literal or pattern to convert') + + // Get the text of the node to reformat it. + const original = ctx.getTextOf(target) + // Replace line breaks with spaces and remove extra spaces. + let oneLine = original.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim() + // Remove a comma that comes before a closing bracket or brace. + oneLine = oneLine.replace(/,\s*([}\]])/g, '$1') + + if (target.type === 'ArrayExpression' || target.type === 'ArrayPattern') { + // For arrays, add a missing space before a closing bracket. + oneLine = oneLine.replace(/\[\s+/g, '[').replace(/\s+\]/g, ']') + } + else { + // For objects, add a missing space before a closing bracket or brace. + oneLine = oneLine.replace(/([^ \t])([}\]])/g, '$1 $2') + // Add a space between a ']' and a '}' if they touch. + oneLine = oneLine.replace(/\](\})/g, '] $1') + } + + // Fix any nested array formatting. + oneLine = oneLine.replace(/\[\s+/g, '[').replace(/\s+\]/g, ']') + + ctx.report({ + node: target, + message: 'Convert object/array to one line', + fix: fixer => fixer.replaceTextRange(target.range, oneLine), + }) + + function isAllowedType(type: NodeType): boolean { + return ( + type === 'ObjectExpression' + || type === 'ArrayExpression' + || type === 'ObjectPattern' + || type === 'ArrayPattern' + ) + } + }, +}