jfmengels / elm-review

Analyzes Elm projects, to help find mistakes before your users find them.
https://package.elm-lang.org/packages/jfmengels/elm-review/latest/
Other
255 stars 13 forks source link

RFC: Context creator #17

Closed jfmengels closed 4 years ago

jfmengels commented 4 years ago

I have been playing (and doing a lot of refactoring to make it work) with creating rules where the context can be initialized with some pre-computed data. This is what it would look like:

rule : Rule
rule =
    Rule.newModuleRuleSchemaWithContextCreator "NoDebug.TodoOrToString" contextCreator
        |> Rule.withImportVisitor importVisitor
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\metadata () ->
            { moduleName = Rule.moduleNameFromMetadata metadata
            , moduleNameNode = Rule.moduleNameNodeFromMetadata metadata
            , isInSourceDirectories = Rule.isInSourceDirectories metadata
            }
        )
        |> Rule.withModuleMetadata

The API for the "context creator" is similar to the JSON decode pipeline. Every with adds an argument to the function.

Right now, the available information is:

Some others pre-computed that I am thinking of:

The benefit of this system is also that we can compute most of this once, before rules are run, instead of once for every rule that wants that information (and needs to collect it on their own).

Why not give all the information at once?

A few points:

What I am looking for

Feedback

I am looking for feedback on this feature. Do you think this is useful, great, cumbersome, overkill?

Bikeshedding

I am looking for better names to the functions I shown here. Note that I might need to have a separate context creator for module visitors than for project visitors.

---- Review.Rule - MINOR ----

    Added:
        type ContextCreator from to
        type Metadata 
        initContextCreator : (from -> to) -> Review.Rule.ContextCreator from to
        isInSourceDirectories : Review.Rule.Metadata -> Basics.Bool
        moduleNameFromMetadata :
            Review.Rule.Metadata -> Elm.Syntax.ModuleName.ModuleName
        moduleNameNodeFromMetadata :
            Review.Rule.Metadata
            -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName
        newModuleRuleSchemaUsingContextCreator :
            String.String
            -> Review.Rule.ContextCreator () moduleContext
            -> Review.Rule.ModuleRuleSchema {} moduleContext
        withMetadata :
            Review.Rule.ContextCreator Review.Rule.Metadata (from -> to)
            -> Review.Rule.ContextCreator from to
        withModuleContextUsingContextCreator :
            { fromProjectToModule :
                  Review.Rule.ContextCreator projectContext moduleContext
            , fromModuleToProject :
                  Review.Rule.ContextCreator moduleContext projectContext
            , foldProjectContexts :
                  projectContext -> projectContext -> projectContext
            }
            -> Review.Rule.ProjectRuleSchema
                   { schemaState
                       | canAddModuleVisitor : ()
                       , withModuleContext : Review.Rule.Required
                   }
                   projectContext
                   moduleContext
            -> Review.Rule.ProjectRuleSchema
                   { schemaState
                       | hasAtLeastOneVisitor : ()
                       , withModuleContext : Review.Rule.Forbidden
                   }
                   projectContext
                   moduleContext
        withModuleKey :
            Review.Rule.ContextCreator Review.Rule.ModuleKey (from -> to)
            -> Review.Rule.ContextCreator from to

Does it even make sense to have a Metadata field, when we could do withModuleName, withModuleNameNode, withIsInSourceDirectories? I am thinking this might be easier and I am not sure what other fields I might add later to the metadata. Also, I think it's better for re-runs that rules don't store the Metadata itself inside the context (but not sure that will impact anything yet). I am leaning towards that at the moment.

Should ContextCreator be named ContextBuilder, or something else entirely?

People trying it out

Try it out on your rules, and let me know what you think. Let me know if you find nice patterns that we can suggest to use in the documentation. I think the easiest way to try it out for now is to checkout this repo and create a rule in the tests.

You can find the documentation here: https://elm-doc-preview.netlify.app/?repo=jfmengels/elm-review

MartinSStewart commented 4 years ago

This is a lot to think about so I don't think I have any good response to your feedback question.

A lookup table to know the "real" module name of a type/value, based on the range of the Node. Ideally, this would replace elm-review-scope.

That said, this might be a good feature request for elm-syntax?

jfmengels commented 4 years ago

A lookup table to know the "real" module name of a type/value, based on the range of the Node. Ideally, this would replace elm-review-scope.

That said, this might be a good feature request for elm-syntax?

It would, but I do not believe that elm-syntax can do the best job at this, especially with regards to re-computing the lookup table, at least with the current processing approach.

I guess it could do an efficient work if we add all files (with Elm.Processing.addFile) to the processing context, and then post-process them with all the knowledge (Elm.Processing.process).

But imagine module A gets modified after that initial processing (in watch or fix mode) and stops exposing something. That can have an impact on the lookup table of module B that imports module A. elm-syntax doesn't have the current capability to re-generate the lookup table for module B, whereas elm-review already does this kind of work, almost at its core.

I would be glad to be proved wrong though, if you have any ideas :blush:

(cc @stil4m)

MartinSStewart commented 4 years ago

Sorry, I was unclear. I'm thinking that it would be a good feature for elm-syntax to support resolving the module name for a function or type from a list of files. Caching the results from that would still be the responsibility of elm-review.

jfmengels commented 4 years ago

I'm thinking that it would be a good feature for elm-syntax to support resolving the module name for a function or type from a list of files

I am a bit unclear as to what you mean here. Do you mean to be able to tell things like this below?

module B exposing (..)

import Html.Styled as Html
import A exposing (..)

a =
  identity --> this comes from module `Basics`
    b      --> This is local

b =
  Html.div --> this comes from module `Html.Styled`
    attributes --> This comes from module `A`

If so, that is what I meant. In my previous comment, the problem I wanted to highlight, is if A stops exposing attributes, then we'd need to somehow have elm-syntax re-analyse (re-parse?) B to get the correct lookup table.

If you meant something else, then I didn't get it.

MartinSStewart commented 4 years ago

Ah, I misunderstood your question then. Yes, I think you're right then that adding such a feature to elm-syntax wouldn't help elm-review very much.

jfmengels commented 4 years ago

Implemented and released with 2.3.0 :tada: