Skip to content

Commit

Permalink
feat(language-service): noSyncWithConstant refacto (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiamanzati authored Nov 22, 2022
1 parent f4175ad commit 0812ab0
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-papayas-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/language-service": patch
---

Add refactor to handle no-sync-with-constants diagnostic
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Accepted values are: `"none" | "suggestion" | "warning" | "error"`
"plugins": [
{
"name": "@effect/language-service",
"diagnostic": {
"diagnostics": {
"1003": "warning",
"1002": "none"
}
Expand Down
10 changes: 10 additions & 0 deletions examples/refactors/noSyncWithConstant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//7:14
import * as Effect from "@effect/core/io/Effect"
import { pipe } from "@fp-ts/data/Function"


const result = pipe(
Effect.sync(() => "hello"),
Effect.map((hello) => hello + ", world!"),
Effect.flatMap((msg) => Effect.log(msg))
)
53 changes: 24 additions & 29 deletions src/diagnostics/noSyncWithConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@ import * as T from "@effect/core/io/Effect"
import * as AST from "@effect/language-service/ast"
import type { DiagnosticDefinitionMessage } from "@effect/language-service/diagnostics/definition"
import { createDiagnostic } from "@effect/language-service/diagnostics/definition"
import {
findModuleImportIdentifierName,
isCombinatorCall,
isLiteralConstantValue
} from "@effect/language-service/utils"
import { getEffectModuleIdentifier, isCombinatorCall, isLiteralConstantValue } from "@effect/language-service/utils"
import * as Ch from "@tsplus/stdlib/collections/Chunk"
import { pipe } from "@tsplus/stdlib/data/Function"
import * as O from "@tsplus/stdlib/data/Maybe"

export const noSyncWithConstantMethodsMap = {
sync: "succeed",
failSync: "fail",
dieSync: "die"
}

export function isEffectSyncWithConstantCall(ts: AST.TypeScriptApi) {
return (moduleIdentifier: string, methodName: string) =>
(node: ts.Node): node is ts.CallExpression => {
if (isCombinatorCall(ts)(moduleIdentifier, methodName)(node) && node.arguments.length === 1) {
const arg = node.arguments[0]
if (ts.isArrowFunction(arg) && isLiteralConstantValue(ts)(arg.body)) {
return true
}
}
return false
}
}

export default createDiagnostic({
code: 1002,
Expand All @@ -18,36 +32,17 @@ export default createDiagnostic({
T.gen(function*($) {
const ts = yield* $(T.service(AST.TypeScriptApi))

const effectIdentifier = pipe(
findModuleImportIdentifierName(ts)(sourceFile, "@effect/core/io/Effect"),
O.getOrElse(
() => "Effect"
)
)

const methodsMap = {
sync: "succeed",
failSync: "fail",
dieSync: "die"
}
const effectIdentifier = getEffectModuleIdentifier(ts)(sourceFile)

let result: Ch.Chunk<DiagnosticDefinitionMessage> = Ch.empty()

for (const methodName of Object.keys(methodsMap)) {
const suggestedMethodName: string = methodsMap[methodName]!

for (const methodName of Object.keys(noSyncWithConstantMethodsMap)) {
const suggestedMethodName: string = noSyncWithConstantMethodsMap[methodName]!
result = pipe(
result,
Ch.concat(
pipe(
AST.collectAll(ts)(sourceFile, isCombinatorCall(ts)(effectIdentifier, methodName)),
Ch.filter((node) => {
if (node.arguments.length !== 1) return false
const arg = node.arguments[0]!
if (!ts.isArrowFunction(arg)) return false
if (!isLiteralConstantValue(ts)(arg.body)) return false
return true
}),
AST.collectAll(ts)(sourceFile, isEffectSyncWithConstantCall(ts)(effectIdentifier, methodName)),
Ch.map((node) => ({
node,
messageText: `Value is constant, instead of using ${methodName} you could use ${suggestedMethodName}.`
Expand Down
5 changes: 2 additions & 3 deletions src/refactors/asyncAwaitToGen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as T from "@effect/core/io/Effect"
import * as AST from "@effect/language-service/ast"
import { createRefactor } from "@effect/language-service/refactors/definition"
import { findModuleImportIdentifierName, transformAsyncAwaitToEffectGen } from "@effect/language-service/utils"
import { getEffectModuleIdentifier, transformAsyncAwaitToEffectGen } from "@effect/language-service/utils"
import * as Ch from "@tsplus/stdlib/collections/Chunk"
import { pipe } from "@tsplus/stdlib/data/Function"
import * as O from "@tsplus/stdlib/data/Maybe"
Expand All @@ -23,8 +23,7 @@ export default createRefactor({
description: "Rewrite to Effect.gen",
apply: T.gen(function*($) {
const changeTracker = yield* $(T.service(AST.ChangeTrackerApi))
const importedEffectName = (findModuleImportIdentifierName(ts)(sourceFile, "@effect/core/io/Effect"))
const effectName = pipe(importedEffectName, O.getOrElse(() => "Effect"))
const effectName = getEffectModuleIdentifier(ts)(sourceFile)

const newDeclaration = transformAsyncAwaitToEffectGen(ts)(node, effectName, (expression) =>
ts.factory.createCallExpression(
Expand Down
5 changes: 2 additions & 3 deletions src/refactors/asyncAwaitToGenTryPromise.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as T from "@effect/core/io/Effect"
import * as AST from "@effect/language-service/ast"
import { createRefactor } from "@effect/language-service/refactors/definition"
import { findModuleImportIdentifierName, transformAsyncAwaitToEffectGen } from "@effect/language-service/utils"
import { getEffectModuleIdentifier, transformAsyncAwaitToEffectGen } from "@effect/language-service/utils"
import * as Ch from "@tsplus/stdlib/collections/Chunk"
import { pipe } from "@tsplus/stdlib/data/Function"
import * as O from "@tsplus/stdlib/data/Maybe"
Expand All @@ -23,8 +23,7 @@ export default createRefactor({
description: "Rewrite to Effect.gen with failures",
apply: T.gen(function*($) {
const changeTracker = yield* $(T.service(AST.ChangeTrackerApi))
const importedEffectName = (findModuleImportIdentifierName(ts)(sourceFile, "@effect/core/io/Effect"))
const effectName = pipe(importedEffectName, O.getOrElse(() => "Effect"))
const effectName = getEffectModuleIdentifier(ts)(sourceFile)

let errorCount = 0

Expand Down
4 changes: 3 additions & 1 deletion src/refactors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import addPipe from "@effect/language-service/refactors/addPipe"
import asyncAwaitToGen from "@effect/language-service/refactors/asyncAwaitToGen"
import asyncAwaitToGenTryPromise from "@effect/language-service/refactors/asyncAwaitToGenTryPromise"
import functionToArrow from "@effect/language-service/refactors/functionToArrow"
import noSyncWithConstant from "@effect/language-service/refactors/noSyncWithConstant"
import removeCurryArrow from "@effect/language-service/refactors/removeCurryArrow"
import removePipe from "@effect/language-service/refactors/removePipe"
import toggleReturnTypeAnnotation from "@effect/language-service/refactors/toggleReturnTypeAnnotation"
Expand All @@ -17,5 +18,6 @@ export default {
functionToArrow,
toggleTypeAnnotation,
toggleReturnTypeAnnotation,
wrapWithPipe
wrapWithPipe,
noSyncWithConstant
}
60 changes: 60 additions & 0 deletions src/refactors/noSyncWithConstant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as T from "@effect/core/io/Effect"
import * as AST from "@effect/language-service/ast"
import {
isEffectSyncWithConstantCall,
noSyncWithConstantMethodsMap
} from "@effect/language-service/diagnostics/noSyncWithConstant"
import { createRefactor } from "@effect/language-service/refactors/definition"
import { getEffectModuleIdentifier, isLiteralConstantValue } from "@effect/language-service/utils"
import * as Ch from "@tsplus/stdlib/collections/Chunk"
import { pipe } from "@tsplus/stdlib/data/Function"
import * as O from "@tsplus/stdlib/data/Maybe"

export default createRefactor({
name: "effect/addPipe",
description: "Rewrite using pipe",
apply: (sourceFile, textRange) =>
T.gen(function*($) {
const ts = yield* $(T.service(AST.TypeScriptApi))
const effectIdentifier = getEffectModuleIdentifier(ts)(sourceFile)

const nodes = pipe(
AST.getNodesContainingRange(ts)(sourceFile, textRange),
Ch.reverse,
Ch.from,
Ch.filter(AST.isNodeInRange(textRange))
)

for (const methodName of Object.keys(noSyncWithConstantMethodsMap)) {
const suggestedMethodName: string = noSyncWithConstantMethodsMap[methodName]!
const refactor = pipe(
nodes,
Ch.filter(isEffectSyncWithConstantCall(ts)(effectIdentifier, methodName)),
Ch.head,
O.map((node) => ({
description: `Rewrite ${methodName} to ${suggestedMethodName}`,
apply: T.gen(function*($) {
const changeTracker = yield* $(T.service(AST.ChangeTrackerApi))
const arg = node.arguments[0]
if (ts.isArrowFunction(arg) && isLiteralConstantValue(ts)(arg.body)) {
const newNode = ts.factory.updateCallExpression(
node,
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(effectIdentifier),
suggestedMethodName
),
node.typeArguments,
ts.factory.createNodeArray([arg.body])
)

changeTracker.replaceNode(sourceFile, node, newNode)
}
})
}))
)
if (O.isSome(refactor)) return refactor
}

return O.none
})
})
15 changes: 13 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ export function isCurryArrow(ts: AST.TypeScriptApi) {
}

export function isLiteralConstantValue(ts: AST.TypeScriptApi) {
return (node: ts.Node) => {
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
return (node: ts.Node): node is (ts.StringLiteral | ts.NumericLiteral | ts.TrueLiteral | ts.FalseLiteral) => {
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
node.kind === ts.SyntaxKind.TrueKeyword ||
node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
}
Expand Down Expand Up @@ -241,3 +241,14 @@ export function removeReturnTypeAnnotation(
}
}
}

export function getEffectModuleIdentifier(ts: AST.TypeScriptApi) {
return (sourceFile: ts.SourceFile) =>
pipe(
findModuleImportIdentifierName(ts)(sourceFile, "@effect/core/io/Effect"),
O.orElse(() => findModuleImportIdentifierName(ts)(sourceFile, "@effect/io/Effect")),
O.getOrElse(
() => "Effect"
)
)
}
3 changes: 0 additions & 3 deletions test/__snapshots__/diagnostics.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ exports[`noSyncWithConstant.ts > noSyncWithConstant.ts 1`] = `
T.sync(() => true)
4:10 - 4:29 | Value is constant, instead of using sync you could use succeed.
T.sync(() => stuff)
11:10 - 11:30 | Value is constant, instead of using sync you could use succeed.
T.failSync(() => 42)
13:10 - 13:31 | Value is constant, instead of using failSync you could use fail.
Expand Down
14 changes: 14 additions & 0 deletions test/__snapshots__/refactors.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ class Sample {
"
`;

exports[`noSyncWithConstant.ts > noSyncWithConstant.ts at 7:14 1`] = `
"// Result of running refactor effect/addPipe at position 7:14
import * as Effect from \\"@effect/core/io/Effect\\"
import { pipe } from \\"@fp-ts/data/Function\\"
const result = pipe(
Effect.succeed(\\"hello\\"),
Effect.map((hello) => hello + \\", world!\\"),
Effect.flatMap((msg) => Effect.log(msg))
)
"
`;

exports[`removeCurryArrow.ts > removeCurryArrow.ts at 7:12 1`] = `
"// Result of running refactor effect/removeCurryArrow at position 7:12
import * as T from \\"@effect/core/io/Effect\\"
Expand Down

0 comments on commit 0812ab0

Please sign in to comment.