Repository: nicoespeon/abracadabra (ts)

Issue:
New refactoring: Yoda flip
## Is this request related to a problem? Please describe.
Some people love [Yoda conditions](https://en.wikipedia.org/wiki/Yoda_conditions), some people hate them. While Yoda conditions do have their [pros and cons](https://knowthecode.io/yoda-conditions-yoda-not-yoda) flipping a condition involves cut'n'paste, some cursor movements and sometimes logic reverse. It may be not an issue for a couple of places in code but when refactoring a big code base this raises the amount of "bleeps" quite significantly. 

## Describe the solution you'd like
`'🙂' === [cursor]emotion` -> `Ctrl+.` -> `Yoda flip` -> `emotion[cursor] === '🙂'`

Refactoring should also account for comparison operator:
- `==`, `===`, `!=`, `!==` should remain unchanged
- `>`, `>=`, `<`, `<=` should be flipped





Pull request:
Add "Flip Yoda Condition" refactoring
Closes #749 
`+`, `*`, `&`, `|`, `^` can also be flipped, but maybe the name should be changed since they're not conditions.
At least with numbers it behaves the same but I just noticed that `+` changes the order of strings when flipping.. ignore, add type checking or exclude this operator?

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c69bbce4..91b01f88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - **[New Refactoring]** "Change Signature" thanks to @11joselu's great work to find function references across files.
 
+- **[New Refactoring]** "Flip Yoda Condition"
+
 ### Fixed
 
 - "Toggle Braces" and "Convert to ForEach" used to transform the outside-most wrapper that would match the transformation. Now, they transform the selected target, even if it's deeply nested. Props to @j4k0xb for finding this one.
diff --git a/package.json b/package.json
index b4e1fed5..c7c4c148 100644
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
     "onCommand:abracadabra.extractInterface",
     "onCommand:abracadabra.flipIfElse",
     "onCommand:abracadabra.flipTernary",
+    "onCommand:abracadabra.flipYodaCondition",
     "onCommand:abracadabra.inline",
     "onCommand:abracadabra.liftUpConditional",
     "onCommand:abracadabra.mergeIfStatements",
@@ -267,6 +268,11 @@
         "title": "Flip Ternary",
         "category": "Abracadabra"
       },
+      {
+        "command": "abracadabra.flipYodaCondition",
+        "title": "Flip Yoda Condition",
+        "category": "Abracadabra"
+      },
       {
         "command": "abracadabra.inline",
         "title": "Inline",
@@ -500,6 +506,11 @@
           "default": true,
           "description": "Check if it should appear in the Quick Fix suggestions when it can be executed"
         },
+        "abracadabra.flipYodaCondition.showInQuickFix": {
+          "type": "boolean",
+          "default": true,
+          "description": "Check if it should appear in the Quick Fix suggestions when it can be executed"
+        },
         "abracadabra.inline.showInQuickFix": {
           "type": "boolean",
           "default": true,
@@ -1014,6 +1025,30 @@
           "command": "abracadabra.flipTernary",
           "when": "editorLangId == svelte"
         },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == javascript"
+        },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == javascriptreact"
+        },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == typescript"
+        },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == typescriptreact"
+        },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == vue"
+        },
+        {
+          "command": "abracadabra.flipYodaCondition",
+          "when": "editorLangId == svelte"
+        },
         {
           "command": "abracadabra.inline",
           "when": "editorLangId == javascript"
diff --git a/src/editor/error-reason.ts b/src/editor/error-reason.ts
index fa146fc0..645daead 100644
--- a/src/editor/error-reason.ts
+++ b/src/editor/error-reason.ts
@@ -1,4 +1,5 @@
 export enum ErrorReason {
+  DidNotFindYodaCondition,
   CantChangeSignature,
   DidNotFindClass,
   DidNotFindObjectToDestructure,
@@ -60,6 +61,9 @@ export enum ErrorReason {
 
 export function toString(reason: ErrorReason): string {
   switch (reason) {
+    case ErrorReason.DidNotFindYodaCondition:
+      return didNotFind("a binary expression to flip");
+
     case ErrorReason.CantChangeSignature:
       return cantDoIt("change function signature");
 
diff --git a/src/extension.ts b/src/extension.ts
index 3b948051..79e314c4 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -22,6 +22,7 @@ import extractGenericType from "./refactorings/extract-generic-type";
 import extractInterface from "./refactorings/extract-interface";
 import flipIfElse from "./refactorings/flip-if-else";
 import flipTernary from "./refactorings/flip-ternary";
+import flipYodaCondition from "./refactorings/flip-yoda-condition";
 import inline from "./refactorings/inline";
 import liftUpConditional from "./refactorings/lift-up-conditional";
 import mergeIfStatements from "./refactorings/merge-if-statements";
@@ -98,6 +99,7 @@ const refactorings: { [key: string]: ConfiguredRefactoring } = {
       createFactoryForConstructor,
       flipIfElse,
       flipTernary,
+      flipYodaCondition,
       inline,
       liftUpConditional,
       mergeIfStatements,
diff --git a/src/refactorings/flip-yoda-condition/flip-yoda-condition.ts b/src/refactorings/flip-yoda-condition/flip-yoda-condition.ts
new file mode 100644
index 00000000..fbcc78f3
--- /dev/null
+++ b/src/refactorings/flip-yoda-condition/flip-yoda-condition.ts
@@ -0,0 +1,52 @@
+import { Editor, ErrorReason } from "../../editor/editor";
+import { Selection } from "../../editor/selection";
+import * as t from "../../ast";
+
+export async function flipYodaCondition(editor: Editor) {
+  const { code, selection } = editor;
+  const updatedCode = updateCode(t.parse(code), selection);
+
+  if (!updatedCode.hasCodeChanged) {
+    editor.showError(ErrorReason.DidNotFindYodaCondition);
+    return;
+  }
+
+  await editor.write(updatedCode.code);
+}
+
+const flippedOperators = {
+  "==": "==",
+  "===": "===",
+  "!=": "!=",
+  "!==": "!==",
+  ">": "<",
+  "<": ">",
+  ">=": "<=",
+  "<=": ">="
+} as const;
+
+function updateCode(ast: t.AST, selection: Selection): t.Transformed {
+  return t.transformAST(
+    ast,
+    createVisitor(selection, (path) => {
+      const { node } = path;
+      [node.left, node.right] = [node.right, node.left as t.Expression];
+      node.operator =
+        flippedOperators[node.operator as keyof typeof flippedOperators];
+    })
+  );
+}
+
+export function createVisitor(
+  selection: Selection,
+  onMatch: (path: t.NodePath<t.BinaryExpression>) => void
+): t.Visitor {
+  return {
+    BinaryExpression(path) {
+      if (!selection.isInsidePath(path)) return;
+      if (!(path.node.operator in flippedOperators)) return;
+
+      onMatch(path);
+    }
+  };
+}
diff --git a/src/refactorings/flip-yoda-condition/index.ts b/src/refactorings/flip-yoda-condition/index.ts
new file mode 100644
index 00000000..6f2ea0e8
--- /dev/null
+++ b/src/refactorings/flip-yoda-condition/index.ts
@@ -0,0 +1,17 @@
+import { flipYodaCondition, createVisitor } from "./flip-yoda-condition";
+
+import { RefactoringWithActionProvider } from "../../types";
+
+const config: RefactoringWithActionProvider = {
+  command: {
+    key: "flipYodaCondition",
+    operation: flipYodaCondition,
+    title: "Flip Yoda Condition"
+  },
+  actionProvider: {
+    message: "Flip yoda condition",
+    createVisitor
+  }
+};
+
+export default config;


Test patch:
diff --git a/src/refactorings/flip-yoda-condition/flip-yoda-condition.test.ts b/src/refactorings/flip-yoda-condition/flip-yoda-condition.test.ts
new file mode 100644
index 00000000..653ba133
--- /dev/null
+++ b/src/refactorings/flip-yoda-condition/flip-yoda-condition.test.ts
@@ -0,0 +1,71 @@
+import { InMemoryEditor } from "../../editor/adapters/in-memory-editor";
+import { Code, ErrorReason } from "../../editor/editor";
+import { testEach } from "../../tests-helpers";
+
+import { flipYodaCondition } from "./flip-yoda-condition";
+
+describe("Flip Yoda Condition", () => {
+  testEach<{ code: Code; expected: Code }>(
+    "should flip yoda condition",
+    [
+      {
+        description: "loose equality",
+        code: "a ==[cursor] b",
+        expected: "b == a"
+      },
+      {
+        description: "strict equality",
+        code: "a ===[cursor] b",
+        expected: "b === a"
+      },
+      {
+        description: "loose inequality",
+        code: "a !=[cursor] b",
+        expected: "b != a"
+      },
+      {
+        description: "strict inequality",
+        code: "a !==[cursor] b",
+        expected: "b !== a"
+      },
+      {
+        description: "greather than",
+        code: "a >[cursor] b",
+        expected: "b < a"
+      },
+      {
+        description: "lower than",
+        code: "a <[cursor] b",
+        expected: "b > a"
+      },
+      {
+        description: "greater or equal",
+        code: "a >=[cursor] b",
+        expected: "b <= a"
+      },
+      {
+        description: "lower or equal",
+        code: "a <=[cursor] b",
+        expected: "b >= a"
+      }
+    ],
+    async ({ code, expected }) => {
+      const editor = new InMemoryEditor(code);
+
+      await flipYodaCondition(editor);
+
+      expect(editor.code).toBe(expected);
+    }
+  );
+
+  it("should show an error message if refactoring can't be made", async () => {
+    const editor = new InMemoryEditor(`a in[cursor] b`);
+    jest.spyOn(editor, "showError");
+
+    await flipYodaCondition(editor);
+
+    expect(editor.showError).toBeCalledWith(
+      ErrorReason.DidNotFindYodaCondition
+    );
+  });
+});