tomblachut / svelte-intellij

Svelte components in WebStorm and friends
MIT License
485 stars 38 forks source link

Support Find Usages for subscribed stores #146

Closed tomblachut closed 4 years ago

tomblachut commented 4 years ago

This PR fixes Find Usages dialog, making store references two-way so to speak.

image

Next steps: (minor ones first because they are shorter)

TODO

export interface Readable<T> {
    /**
     * Subscribe on value changes.
     * @param run subscription callback
     * @param invalidate cleanup callback
     */
    subscribe(run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber;
}

type Subscriber<T> = (value: T) => void;

https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts

~AFAIK the way to go about types is to attach JSImplicitElement to JSVariable, and change resolving so the reference resolves to implicit element instead of real one. I know how to do that.~ More challenging thing for me is to configure getJSType to return correct type, T in previous snippet.

@anstarovoyt feedback or some examples would be great

anstarovoyt commented 4 years ago

some thoughts about this part:

AFAIK the way to go about types is to attach JSImplicitElement to JSVariable, and change resolving so the reference resolves to implicit element instead of real one. I know how to do that. More challenging thing for me is to configure getJSType to return correct type, T in previous snippet.

Usually we consider JSImplicitElement as last-possible-solution because it produces some additional issues around searches / refactoring / etc. Actually you can easily extend resolution in JS using some extension e.g. implementing

com.intellij.lang.javascript.index.FrameworkIndexingHandler#addTypeFromResolveResult()

e.g. like here: VuexFrameworkHandler.kt#L133

tomblachut commented 4 years ago

@anstarovoyt Hmm it was easier than I thought, thx.

(usages differ only by subscription operator) image image

class SvelteFrameworkHandler : FrameworkIndexingHandler() {
    override fun addTypeFromResolveResult(
        evaluator: JSTypeEvaluator,
        context: JSEvaluateContext,
        result: PsiElement
    ): Boolean {
        val expression = context.processedExpression
        if (result is JSVariable && expression != null && isSubscribedReference(expression)) {
            try {
                val type = result.jsType?.substitute() ?: return false
                if (type is JSGenericTypeImpl) {
                    evaluator.addType(type.arguments[0], expression)
                    return true
                }
            } catch (e: Exception) {
            }
        }
        return false
    }

    fun isSubscribedReference(expression: JSReferenceExpression): Boolean {
        val name = expression.referenceName
        return (name != null && expression.qualifier == null && name.length > 2 && name[0] == '$' && name[1] != '$')
    }
}

Are there any utils I should be using instead of direct jsType manipulation? I guess I should also check if interface has name Readable somewhere in the prototype

anstarovoyt commented 4 years ago

I guess I should also check if interface has name Readable somewhere in the prototype

Actually I cannot say that it is a common task because in TypeScript (for JavaScript we also use TypeScript evaluation rules) type-checking is structural. But there is for example

com.intellij.lang.javascript.psi.types.JSTypeCastUtil#isDirectlyAssignableInClassHierarchy

it does something like this.

Are there any utils I should be using instead of direct jsType manipulation?

usually, it is ok to process js types directly.

tomblachut commented 4 years ago

type-checking is structural

You're totally right, I didn't think that through. Store contract requires subscribe method with 1st argument of type T, e.g. RxJS is also supported