-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
Rule.elm
6592 lines (5243 loc) · 279 KB
/
Rule.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
module Review.Rule exposing
( Rule
, ModuleRuleSchema, newModuleRuleSchema, fromModuleRuleSchema
, withSimpleModuleDefinitionVisitor, withSimpleCommentsVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
, newModuleRuleSchemaUsingContextCreator
, withModuleDefinitionVisitor
, withModuleDocumentationVisitor
, withCommentsVisitor
, withImportVisitor
, Direction(..), withDeclarationEnterVisitor, withDeclarationExitVisitor, withDeclarationVisitor, withDeclarationListVisitor
, withExpressionEnterVisitor, withExpressionExitVisitor, withExpressionVisitor
, withCaseBranchEnterVisitor, withCaseBranchExitVisitor
, withLetDeclarationEnterVisitor, withLetDeclarationExitVisitor
, providesFixesForModuleRule
, withFinalModuleEvaluation
, withElmJsonModuleVisitor, withReadmeModuleVisitor, withDirectDependenciesModuleVisitor, withDependenciesModuleVisitor
, ProjectRuleSchema, newProjectRuleSchema, fromProjectRuleSchema, withModuleVisitor, withModuleContext, withModuleContextUsingContextCreator, withElmJsonProjectVisitor, withReadmeProjectVisitor, withDirectDependenciesProjectVisitor, withDependenciesProjectVisitor, withFinalProjectEvaluation, withContextFromImportedModules
, providesFixesForProjectRule
, ContextCreator, initContextCreator, withModuleName, withModuleNameNode, withIsInSourceDirectories, withFilePath, withIsFileIgnored, withModuleNameLookupTable, withModuleKey, withSourceCodeExtractor, withFullAst, withModuleDocumentation
, Metadata, withMetadata, moduleNameFromMetadata, moduleNameNodeFromMetadata, isInSourceDirectories
, Error, error, errorWithFix, ModuleKey, errorForModule, errorForModuleWithFix, ElmJsonKey, errorForElmJson, errorForElmJsonWithFix, ReadmeKey, errorForReadme, errorForReadmeWithFix
, globalError, configurationError
, ReviewError, errorRuleName, errorMessage, errorDetails, errorRange, errorFilePath, errorTarget, errorFixes, errorFixFailure
, ignoreErrorsForDirectories, ignoreErrorsForFiles, filterErrorsForFiles
, withDataExtractor, preventExtract
, reviewV3, reviewV2, review, ProjectData, ruleName, ruleProvidesFixes, ruleKnowsAboutIgnoredFiles, withRuleId, getConfigurationError
, Required, Forbidden
)
{-| This module contains functions that are used for writing rules.
**NOTE**: If you want to **create a package** containing `elm-review` rules, I highly recommend using the
[CLI's](https://github.com/jfmengels/node-elm-review/) `elm-review new-package` subcommand. This will create a new package that will help you use the best practices and give you helpful tools like easy auto-publishing. More information is available in the maintenance file generated along with it.
If you want to **add/create a rule** for the package or for your local configuration, then I recommend using `elm-review new-rule`, which will create a source and test file which you can use as a starting point. For packages, it will add the rule everywhere it should be present (`exposed-modules`, README, ...).
# How does it work?
`elm-review` reads the modules, `elm.json`, dependencies and `README.md` from your project,
and turns each module into an [Abstract Syntax Tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree),
a tree-like structure which represents your source code, using the
[`elm-syntax` package](https://package.elm-lang.org/packages/stil4m/elm-syntax/7.2.1/).
`elm-review` then feeds all this data into `review rules`, that traverse them to report problems.
The way that review rules go through the data depends on whether it is a [module rule](#creating-a-module-rule) or a [project rule](#creating-a-project-rule).
`elm-review` relies on the [`elm-syntax` package](https://package.elm-lang.org/packages/stil4m/elm-syntax/7.2.1/),
and all the node types you'll see will be coming from there. You are likely to
need to have the documentation for that package open when writing a rule.
There are plenty of examples in this documentation, and you can also look at the
source code of existing rules to better grasp how they work.
**NOTE**: These examples are only here to showcase how to write rules and how a function can
be used. They are not necessarily good rules to enforce. See the
[section on whether to write a rule](./#when-to-write-or-enable-a-rule) for more on that.
Even if you think they are good ideas to enforce, they are often not complete, as there are other
patterns you would want to forbid, but that are not handled by the example.
# What makes a good rule
Apart from the rationale on [whether a rule should be written](./#when-to-write-or-enable-a-rule),
here are a few tips on what makes a rule helpful.
A review rule is an automated communication tool which sends messages to
developers who have written patterns your rule wishes to prevent. As all
communication, the message is important.
## A good rule name
The name of the rule (`NoUnusedVariables`, `NoDebug`, ...) should try to convey
really quickly what kind of pattern we're dealing with. Ideally, a user who
encounters this pattern for the first time could guess the problem just from the
name. And a user who encountered it several times should know how to fix the
problem just from the name too.
I recommend having the name of the module containing the rule be the same as the
rule name. This will make it easier to find the module in the project or on
the packages website when trying to get more information.
## A helpful error message and details
The error message should give more information about the problem. It is split
into two parts:
- The `message`: A short sentence that describes the forbidden pattern. A user
that has encountered this error multiple times should know exactly what to do.
Example: "Function `foo` is never used". With this information, a user who
knows the rule probably knows that a function needs to be removed from the
source code, and also knows which one.
- The `details`: All the additional information that can be useful to the
user, such as the rationale behind forbidding the pattern, and suggestions
for a solution or alternative.
When writing the error message that the user will see, try to make them be as
helpful as the messages the compiler gives you when it encounters a problem.
## The smallest section of code that makes sense
When creating an error, you need to specify under which section of the code this
message appears. This is where you would see squiggly lines in your editor when
you have review or compiler errors.
To make the error easier to spot, it is best to make this section as small as
possible, as long as that makes sense. For instance, in a rule that would forbid
`Debug.log`, you would the error to appear under `Debug.log`, not on the whole
function which contains this piece of code.
## Good rule documentation
The rule documentation should give the same information as what you would see in
the error message.
If published in a package, the rule documentation should explain when not to
enable the rule in the user's review configuration. For instance, for a rule that
makes sure that a package is publishable by ensuring that all docs are valid,
the rule might say something along the lines of "If you are writing an
application, then you should not use this rule".
Additionally, it could give a few examples of patterns that will be reported and
of patterns that will not be reported, so that users can have a better grasp of
what to expect.
# Strategies for writing rules effectively
## Use Test-Driven Development
This package comes with [`Review.Test`](./Review-Test), which works with [`elm-test`](https://github.com/elm-explorations/test).
I recommend reading through [`the strategies for effective testing`](./Review-Test#strategies-for-effective-testing) before
starting writing a rule.
## Look at the documentation for [`elm-syntax`](https://package.elm-lang.org/packages/stil4m/elm-syntax/7.2.1/)
`elm-review` is heavily dependent on the types that [`elm-syntax`](https://package.elm-lang.org/packages/stil4m/elm-syntax/7.2.1/)
provides. If you don't understand the AST it provides, you will have a hard time
implementing the rule you wish to create.
# Writing a Rule
@docs Rule
## Creating a module rule
A "module rule" looks at modules (i.e. files) one by one. When it finishes looking at a module and reporting errors,
it forgets everything about the module it just analyzed before starting to look at a different module. You should create one of these if you
do not need to know the contents of a different module in the project, such as what functions are exposed.
If you do need that information, you should create a [project rule](#creating-a-project-rule).
If you are new to writing rules, I would recommend learning how to build a module rule first, as they are in practice a
simpler version of project rules.
The traversal of a module rule is the following:
- Read project-related info (only collect data in the context in these steps)
- The `elm.json` file, visited by [`withElmJsonModuleVisitor`](#withElmJsonModuleVisitor)
- The `README.md` file, visited by [`withReadmeModuleVisitor`](#withReadmeModuleVisitor)
- The definition for dependencies, visited by [`withDirectDependenciesModuleVisitor`](#withDirectDependenciesModuleVisitor) and [`withDependenciesModuleVisitor`](#withDependenciesModuleVisitor)
- Visit the Elm module (in the following order)
- The module definition, visited by [`withSimpleModuleDefinitionVisitor`](#withSimpleModuleDefinitionVisitor) and [`withModuleDefinitionVisitor`](#withModuleDefinitionVisitor)
- The module documentation, visited by [`withModuleDocumentationVisitor`](#withModuleDocumentationVisitor)
- The module's list of comments, visited by [`withSimpleCommentsVisitor`](#withSimpleCommentsVisitor) and [`withCommentsVisitor`](#withCommentsVisitor)
- Each import, visited by [`withSimpleImportVisitor`](#withSimpleImportVisitor) and [`withImportVisitor`](#withImportVisitor)
- The list of declarations, visited by [`withDeclarationListVisitor`](#withDeclarationListVisitor)
- Each declaration, visited in the following order:
- [`withSimpleDeclarationVisitor`](#withSimpleDeclarationVisitor) and [`withDeclarationEnterVisitor`](#withDeclarationEnterVisitor)
- The expression contained in the declaration will be visited recursively by [`withSimpleExpressionVisitor`](#withSimpleExpressionVisitor), [`withExpressionEnterVisitor`](#withExpressionEnterVisitor) and [`withExpressionExitVisitor`](#withExpressionExitVisitor).
- [`withDeclarationExitVisitor`](#withDeclarationExitVisitor)
- A final evaluation is made when the module has fully been visited, using [`withFinalModuleEvaluation`](#withFinalModuleEvaluation)
Evaluating/visiting a node means two things:
- Detecting patterns and reporting errors
- Collecting data in a "context" (called `moduleContext` for module rules) to have more information available in a later
node evaluation. You can only use the context and update it with "non-simple with\*" visitor functions.
I recommend using the "simple with\*" visitor functions if you don't need to do either, as they are simpler to use
@docs ModuleRuleSchema, newModuleRuleSchema, fromModuleRuleSchema
## Builder functions without context
@docs withSimpleModuleDefinitionVisitor, withSimpleCommentsVisitor, withSimpleImportVisitor, withSimpleDeclarationVisitor, withSimpleExpressionVisitor
## Builder functions with context
@docs newModuleRuleSchemaUsingContextCreator
@docs withModuleDefinitionVisitor
@docs withModuleDocumentationVisitor
@docs withCommentsVisitor
@docs withImportVisitor
@docs Direction, withDeclarationEnterVisitor, withDeclarationExitVisitor, withDeclarationVisitor, withDeclarationListVisitor
@docs withExpressionEnterVisitor, withExpressionExitVisitor, withExpressionVisitor
@docs withCaseBranchEnterVisitor, withCaseBranchExitVisitor
@docs withLetDeclarationEnterVisitor, withLetDeclarationExitVisitor
@docs providesFixesForModuleRule
@docs withFinalModuleEvaluation
## Builder functions to analyze the project's data
@docs withElmJsonModuleVisitor, withReadmeModuleVisitor, withDirectDependenciesModuleVisitor, withDependenciesModuleVisitor
## Creating a project rule
Project rules can look at the global picture of an Elm project. Contrary to module
rules, which forget everything about the module they were looking at when going from
one module to another, project rules can retain information about previously
analyzed modules, and use it to report errors when analyzing a different module or
after all modules have been visited.
Project rules can also report errors in the `elm.json` or the `README.md` files.
If you are new to writing rules, I would recommend learning [how to build a module rule](#creating-a-module-rule)
first, as they are in practice a simpler version of project rules.
@docs ProjectRuleSchema, newProjectRuleSchema, fromProjectRuleSchema, withModuleVisitor, withModuleContext, withModuleContextUsingContextCreator, withElmJsonProjectVisitor, withReadmeProjectVisitor, withDirectDependenciesProjectVisitor, withDependenciesProjectVisitor, withFinalProjectEvaluation, withContextFromImportedModules
@docs providesFixesForProjectRule
## Requesting more information
@docs ContextCreator, initContextCreator, withModuleName, withModuleNameNode, withIsInSourceDirectories, withFilePath, withIsFileIgnored, withModuleNameLookupTable, withModuleKey, withSourceCodeExtractor, withFullAst, withModuleDocumentation
### Requesting more information (DEPRECATED)
@docs Metadata, withMetadata, moduleNameFromMetadata, moduleNameNodeFromMetadata, isInSourceDirectories
## Errors
@docs Error, error, errorWithFix, ModuleKey, errorForModule, errorForModuleWithFix, ElmJsonKey, errorForElmJson, errorForElmJsonWithFix, ReadmeKey, errorForReadme, errorForReadmeWithFix
@docs globalError, configurationError
@docs ReviewError, errorRuleName, errorMessage, errorDetails, errorRange, errorFilePath, errorTarget, errorFixes, errorFixFailure
## Configuring exceptions
There are situations where you don't want review rules to report errors:
1. You copied and updated over an external library because one of your needs wasn't met, and you don't want to modify it more than necessary.
2. Your project contains generated source code, over which you have no control or for which you do not care that some rules are enforced (like the reports of unused variables).
3. You want to introduce a rule progressively, because there are too many errors in the project for you to fix in one go. You can then ignore the parts of the project where the problem has not yet been solved, and fix them as you go.
4. You wrote a rule that is very specific and should only be applied to a portion of your code.
5. You wish to disable some rules for tests files (or enable some only for tests).
You can use the following functions to ignore errors in directories or files, or only report errors found in specific directories or files.
**NOTE**: Even though they can be used to disable any errors, I **strongly recommend against**
doing so if you are not in the situations listed above. I highly recommend you
leave a comment explaining the reason why you use these functions, or to
communicate with your colleagues if you see them adding exceptions without
reason or seemingly inappropriately.
@docs ignoreErrorsForDirectories, ignoreErrorsForFiles, filterErrorsForFiles
## Extract information
As you might have seen so far, `elm-review` has quite a nice way of traversing the files of a project and collecting data.
While you have only seen the tool be used to report errors, you can also use it to extract information from
the codebase. You can use this to gain insight into your codebase, or provide information to other tools to enable
powerful integrations.
You can read more about how to use this in [_Extract information_ in the README](./#extract-information), and you can
find the tools to extract data below.
@docs withDataExtractor, preventExtract
# Running rules
@docs reviewV3, reviewV2, review, ProjectData, ruleName, ruleProvidesFixes, ruleKnowsAboutIgnoredFiles, withRuleId, getConfigurationError
# Internals
@docs Required, Forbidden
-}
import Dict exposing (Dict)
import Elm.Project
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Expression as Expression exposing (Expression, Function)
import Elm.Syntax.File exposing (File)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Infix as Infix
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import Json.Encode as Encode
import Review.Cache.ContentHash exposing (ContentHash)
import Review.Cache.ContextHash as ContextHash exposing (ComparableContextHash, ContextHash)
import Review.Cache.EndAnalysis as EndAnalysisCache
import Review.Cache.Module as ModuleCache
import Review.Cache.ProjectFile as ProjectFileCache
import Review.ElmProjectEncoder
import Review.Error exposing (InternalError)
import Review.Exceptions as Exceptions exposing (Exceptions)
import Review.FilePath exposing (FilePath)
import Review.Fix as Fix exposing (Fix)
import Review.Fix.FixProblem as FixProblem
import Review.Fix.FixedErrors as FixedErrors exposing (FixedErrors)
import Review.Fix.Internal as InternalFix
import Review.ImportCycle as ImportCycle
import Review.Logger as Logger
import Review.ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.ModuleNameLookupTable.Compute
import Review.ModuleNameLookupTable.Internal as ModuleNameLookupTableInternal
import Review.Options as ReviewOptions exposing (ReviewOptions)
import Review.Options.Internal as InternalOptions exposing (ReviewOptionsData, ReviewOptionsInternal(..))
import Review.Project.Dependency
import Review.Project.Internal exposing (Project)
import Review.Project.InvalidProjectError as InvalidProjectError
import Review.Project.ProjectModule as ProjectModule exposing (OpaqueProjectModule)
import Review.Project.Valid as ValidProject exposing (ValidProject)
import Review.RequestedData as RequestedData exposing (RequestedData(..))
import Vendor.Graph as Graph exposing (Graph)
import Vendor.IntDict as IntDict
import Vendor.Zipper as Zipper exposing (Zipper)
{-| Represents a construct able to analyze a project and report
unwanted patterns.
You can create [module rules](#creating-a-module-rule) or [project rules](#creating-a-project-rule).
-}
type Rule
= Rule
{ name : String
, id : Int
, exceptions : Exceptions
, requestedData : RequestedData
, providesFixes : Bool
, ruleProjectVisitor : Result { message : String, details : List String } (ValidProject -> ChangeableRuleData -> RuleProjectVisitor)
}
{-| Represents a schema for a module [`Rule`](#Rule).
Start by using [`newModuleRuleSchema`](#newModuleRuleSchema), then add visitors to look at the parts of the code you are interested in.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebug" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
-}
type ModuleRuleSchema schemaState moduleContext
= ModuleRuleSchema (ModuleRuleSchemaData moduleContext)
type alias ModuleRuleSchemaData moduleContext =
{ name : String
, initialModuleContext : Maybe moduleContext
, moduleContextCreator : ContextCreator () moduleContext
, moduleDefinitionVisitor : Maybe (Visitor Module moduleContext)
, moduleDocumentationVisitor : Maybe (Maybe (Node String) -> moduleContext -> ( List (Error {}), moduleContext ))
, commentsVisitor : Maybe (List (Node String) -> moduleContext -> ( List (Error {}), moduleContext ))
, importVisitor : Maybe (Node Import -> moduleContext -> ( List (Error {}), moduleContext ))
, declarationListVisitor : Maybe (List (Node Declaration) -> moduleContext -> ( List (Error {}), moduleContext ))
, declarationVisitorOnEnter : Maybe (Visitor Declaration moduleContext)
, declarationVisitorOnExit : Maybe (Visitor Declaration moduleContext)
, expressionVisitorsOnEnter : Maybe (Visitor Expression moduleContext)
, expressionVisitorsOnExit : Maybe (Visitor Expression moduleContext)
, letDeclarationVisitorOnEnter : Maybe (Node Expression.LetBlock -> Node Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext ))
, letDeclarationVisitorOnExit : Maybe (Node Expression.LetBlock -> Node Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext ))
, caseBranchVisitorOnEnter : Maybe (Node Expression.CaseBlock -> ( Node Pattern, Node Expression ) -> moduleContext -> ( List (Error {}), moduleContext ))
, caseBranchVisitorOnExit : Maybe (Node Expression.CaseBlock -> ( Node Pattern, Node Expression ) -> moduleContext -> ( List (Error {}), moduleContext ))
, finalEvaluationFn : Maybe (moduleContext -> List (Error {}))
, providesFixes : Bool
-- Project visitors
, elmJsonVisitor : Maybe (Maybe Elm.Project.Project -> moduleContext -> moduleContext)
, readmeVisitor : Maybe (Maybe String -> moduleContext -> moduleContext)
, dependenciesVisitor : Maybe (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext)
, directDependenciesVisitor : Maybe (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext)
}
-- REVIEWING
{-| **DEPRECATED:** Use [`reviewV2`](#reviewV2) instead.
Review a project and gives back the errors raised by the given rules.
Note that you won't need to use this function when writing a rule. You should
only need it if you try to make `elm-review` run in a new environment.
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
project : Project
project =
Project.new
|> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
|> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }
doReview =
let
( errors, rulesWithCachedValues ) =
Rule.review rules project
in
doSomethingWithTheseValues
The resulting `List Rule` is the same list of rules given as input, but with an
updated internal cache to make it faster to re-run the rules on the same project.
If you plan on re-reviewing with the same rules and project, for instance to
review the project after a file has changed, you may want to store the rules in
your `Model`.
The rules are functions, so doing so will make your model unable to be
exported/imported with `elm/browser`'s debugger, and may cause a crash if you try
to compare them or the model that holds them.
-}
review : List Rule -> Project -> ( List ReviewError, List Rule )
review rules project =
case ValidProject.parse project of
Err (InvalidProjectError.SomeModulesFailedToParse pathsThatFailedToParse) ->
( List.map parsingError pathsThatFailedToParse, rules )
Err (InvalidProjectError.DuplicateModuleNames duplicate) ->
( [ duplicateModulesGlobalError duplicate ], rules )
Err (InvalidProjectError.ImportCycleError cycle) ->
( [ importCycleError cycle ], rules )
Err InvalidProjectError.NoModulesError ->
( [ elmReviewGlobalError
{ message = "This project does not contain any Elm modules"
, details = [ "I need to look at some Elm modules. Maybe you have specified folders that do not exist?" ]
}
|> setRuleName "Incorrect project"
|> errorToReviewError
]
, rules
)
Ok validProject ->
case checkForConfigurationErrors validProject rules [] of
Err configurationErrors ->
( configurationErrors, rules )
Ok ruleProjectVisitors ->
let
runRulesResult : { errors : List ReviewError, fixedErrors : Dict String (List ReviewError), rules : List Rule, project : Project, extracts : Dict String Encode.Value }
runRulesResult =
runRules ReviewOptions.defaults ruleProjectVisitors validProject
in
( runRulesResult.errors, runRulesResult.rules )
{-| Review a project and gives back the errors raised by the given rules.
Note that you won't need to use this function when writing a rule. You should
only need it if you try to make `elm-review` run in a new environment.
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
project : Project
project =
Project.new
|> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
|> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }
doReview =
let
{ errors, rules, projectData } =
-- Replace `config` by `rules` next time you call reviewV2
-- Replace `Nothing` by `projectData` next time you call reviewV2
Rule.reviewV2 config Nothing project
in
doSomethingWithTheseValues
The resulting `List Rule` is the same list of rules given as input, but with an
updated internal cache to make it faster to re-run the rules on the same project.
If you plan on re-reviewing with the same rules and project, for instance to
review the project after a file has changed, you may want to store the rules in
your `Model`.
The rules are functions, so doing so will make your model unable to be
exported/imported with `elm/browser`'s debugger, and may cause a crash if you try
to compare them or the model that holds them.
-}
reviewV2 : List Rule -> Maybe ProjectData -> Project -> { errors : List ReviewError, rules : List Rule, projectData : Maybe ProjectData }
reviewV2 rules maybeProjectData project =
case getValidProjectAndRules project rules of
Ok ( validProject, ruleProjectVisitors ) ->
runReviewForV2 ReviewOptions.defaults validProject ruleProjectVisitors
Err errors ->
{ errors = errors
, rules = rules
, projectData = maybeProjectData
}
{-| Review a project and gives back the errors raised by the given rules.
Note that you won't need to use this function when writing a rule. You should
only need it if you try to make `elm-review` run in a new environment.
import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
project : Project
project =
Project.new
|> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
|> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }
doReview =
let
{ errors, rules, projectData, extracts } =
-- Replace `config` by `rules` next time you call reviewV2
-- Replace `Nothing` by `projectData` next time you call reviewV2
Rule.reviewV3 config Nothing project
in
doSomethingWithTheseValues
The resulting `List Rule` is the same list of rules given as input, but with an
updated internal cache to make it faster to re-run the rules on the same project.
If you plan on re-reviewing with the same rules and project, for instance to
review the project after a file has changed, you may want to store the rules in
your `Model`.
The rules are functions, so doing so will make your model unable to be
exported/imported with `elm/browser`'s debugger, and may cause a crash if you try
to compare them or the model that holds them.
-}
reviewV3 :
ReviewOptions
-> List Rule
-> Project
->
{ errors : List ReviewError
, fixedErrors : Dict String (List ReviewError)
, rules : List Rule
, project : Project
, extracts : Dict String Encode.Value
}
reviewV3 reviewOptions rules project =
case getValidProjectAndRules project rules of
Ok ( validProject, ruleProjectVisitors ) ->
runRules reviewOptions ruleProjectVisitors validProject
Err errors ->
{ errors = errors
, fixedErrors = Dict.empty
, rules = rules
, project = project
, extracts = Dict.empty
}
getValidProjectAndRules : Project -> List Rule -> Result (List ReviewError) ( ValidProject, List RuleProjectVisitor )
getValidProjectAndRules project rules =
getModulesSortedByImport project
|> Result.andThen
(\validProject ->
checkForConfigurationErrors validProject rules []
|> Result.map (Tuple.pair validProject)
)
checkForConfigurationErrors : ValidProject -> List Rule -> List RuleProjectVisitor -> Result (List ReviewError) (List RuleProjectVisitor)
checkForConfigurationErrors project rules rulesToRunAcc =
case rules of
[] ->
Ok rulesToRunAcc
(Rule rule) :: remainingRules ->
case rule.ruleProjectVisitor of
Ok ruleProjectVisitor ->
checkForConfigurationErrors
project
remainingRules
(ruleProjectVisitor
project
{ exceptions = rule.exceptions
, ruleId = rule.id
, requestedData = rule.requestedData
}
:: rulesToRunAcc
)
Err _ ->
Err (collectConfigurationErrors rules)
collectConfigurationErrors : List Rule -> List ReviewError
collectConfigurationErrors rules =
List.filterMap
(\(Rule rule) ->
case rule.ruleProjectVisitor of
Err { message, details } ->
Just
(Review.Error.ReviewError
{ filePath = "CONFIGURATION ERROR"
, ruleName = rule.name
, message = message
, details = details
, range = Range.emptyRange
, fixes = Review.Error.NoFixes
, target = Review.Error.Global
, preventsExtract = False
}
)
Ok _ ->
Nothing
)
rules
getModulesSortedByImport : Project -> Result (List ReviewError) ValidProject
getModulesSortedByImport project =
case ValidProject.parse project of
Err (InvalidProjectError.SomeModulesFailedToParse pathsThatFailedToParse) ->
Err (List.map parsingError pathsThatFailedToParse)
Err (InvalidProjectError.DuplicateModuleNames duplicate) ->
Err [ duplicateModulesGlobalError duplicate ]
Err (InvalidProjectError.ImportCycleError cycle) ->
Err [ importCycleError cycle ]
Err InvalidProjectError.NoModulesError ->
Err
[ elmReviewGlobalError
{ message = "This project does not contain any Elm modules"
, details = [ "I need to look at some Elm modules. Maybe you have specified folders that do not exist?" ]
}
|> setRuleName "Incorrect project"
|> errorToReviewError
]
Ok result ->
Ok result
importCycleError : List ModuleName -> ReviewError
importCycleError cycle =
ImportCycle.error cycle
|> elmReviewGlobalError
|> setRuleName "Incorrect project"
|> errorToReviewError
runReviewForV2 : ReviewOptions -> ValidProject -> List RuleProjectVisitor -> { errors : List ReviewError, rules : List Rule, projectData : Maybe ProjectData }
runReviewForV2 reviewOptions project ruleProjectVisitors =
let
runResult : { errors : List ReviewError, fixedErrors : Dict String (List ReviewError), rules : List Rule, project : Project, extracts : Dict String Encode.Value }
runResult =
runRules reviewOptions ruleProjectVisitors project
in
{ errors = runResult.errors
, rules = runResult.rules
, projectData = Nothing
}
-- PROJECT DATA
{-| Internal cache about the project.
-}
type
ProjectData
-- This is not used in practice anymore
= ProjectData Never
duplicateModulesGlobalError : { moduleName : ModuleName, paths : List String } -> ReviewError
duplicateModulesGlobalError duplicate =
let
paths : String
paths =
duplicate.paths
|> List.sort
|> List.map (\s -> "\n - " ++ s)
|> String.concat
in
elmReviewGlobalError
{ message = "Found several modules named `" ++ String.join "." duplicate.moduleName ++ "`"
, details =
[ "I found several modules with the name `" ++ String.join "." duplicate.moduleName ++ "`. Depending on how I choose to resolve this, I might give you different reports. Since this is a compiler error anyway, I require this problem to be solved. Please fix this then try running `elm-review` again."
, "Here are the paths to some of the files that share a module name:" ++ paths
, "It is possible that you requested me to look at several projects, and that modules from each project share the same name. I don't recommend reviewing several projects at the same time, as I can only handle one `elm.json`. I instead suggest running `elm-review` twice, once for each project."
]
}
|> errorToReviewError
runRules :
ReviewOptions
-> List RuleProjectVisitor
-> ValidProject
-> { errors : List ReviewError, fixedErrors : Dict String (List ReviewError), rules : List Rule, project : Project, extracts : Dict String Encode.Value }
runRules (ReviewOptionsInternal reviewOptions) ruleProjectVisitors project =
let
result : { fixedErrors : FixedErrors, ruleProjectVisitors : List RuleProjectVisitor, project : ValidProject }
result =
runProjectVisitor
reviewOptions
ruleProjectVisitors
FixedErrors.empty
project
{ errors, rules, extracts } =
computeErrorsAndRulesAndExtracts reviewOptions result.ruleProjectVisitors
in
{ errors = errors
, rules = rules
, extracts = extracts
, fixedErrors = FixedErrors.toDict result.fixedErrors
, project = ValidProject.toRegularProject result.project
}
computeErrorsAndRulesAndExtracts : ReviewOptionsData -> List RuleProjectVisitor -> { errors : List ReviewError, rules : List Rule, extracts : Dict String Encode.Value }
computeErrorsAndRulesAndExtracts reviewOptions ruleProjectVisitors =
if reviewOptions.extract then
List.foldl
(\(RuleProjectVisitor rule) { errors, rules, extracts } ->
let
( newErrors, canComputeExtract ) =
List.foldl
(\(Error err) ( accErrors, canComputeExtract_ ) ->
( Review.Error.ReviewError err :: accErrors
, canComputeExtract_ && not (Review.Error.doesPreventExtract err)
)
)
( errors, True )
(rule.getErrors ())
( newExtracts, RuleProjectVisitor newRule ) =
if canComputeExtract then
rule.dataExtractVisitor reviewOptions extracts
else
( extracts, RuleProjectVisitor rule )
in
{ errors = newErrors
, rules = newRule.backToRule () :: rules
, extracts = newExtracts
}
)
{ errors = [], rules = [], extracts = Dict.empty }
ruleProjectVisitors
else
{ errors =
List.concatMap
(\(RuleProjectVisitor rule) -> rule.getErrors () |> List.map errorToReviewError)
ruleProjectVisitors
, rules = List.map (\(RuleProjectVisitor rule) -> rule.backToRule ()) ruleProjectVisitors
, extracts = Dict.empty
}
{-| Let `elm-review` know that this rule may provide fixes in the reported errors.
This information is hard for `elm-review` to deduce on its own, but can be very useful for improving the performance of
the tool while running in fix mode.
If your rule is a project rule, then you should use [`providesFixesForProjectRule`](#providesFixesForProjectRule) instead.
-}
providesFixesForModuleRule : ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema schemaState moduleContext
providesFixesForModuleRule (ModuleRuleSchema moduleRuleSchema) =
ModuleRuleSchema { moduleRuleSchema | providesFixes = True }
{-| Let `elm-review` know that this rule may provide fixes in the reported errors.
This information is hard for `elm-review` to deduce on its own, but can be very useful for improving the performance of
the tool while running in fix mode.
If your rule is a module rule, then you should use [`providesFixesForModuleRule`](#providesFixesForModuleRule) instead.
-}
providesFixesForProjectRule : ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext
providesFixesForProjectRule (ProjectRuleSchema projectRuleSchema) =
ProjectRuleSchema { projectRuleSchema | providesFixes = True }
{-| Get the name of a rule.
You should not have to use this when writing a rule.
-}
ruleName : Rule -> String
ruleName (Rule rule) =
rule.name
{-| Indicates whether the rule provides fixes.
You should not have to use this when writing a rule.
-}
ruleProvidesFixes : Rule -> Bool
ruleProvidesFixes (Rule rule) =
-- TODO Breaking change: This should be an internal detail, not shown to the user
rule.providesFixes
{-| Indicates whether the rule knows about which files are ignored.
You should not have to use this when writing a rule.
-}
ruleKnowsAboutIgnoredFiles : Rule -> Bool
ruleKnowsAboutIgnoredFiles (Rule rule) =
-- TODO Breaking change: This should be an internal detail, not shown to the user
let
(RequestedData requestedData) =
rule.requestedData
in
requestedData.ignoredFiles
{-| Assign an id to a rule. This id should be unique.
config =
[ rule1, rule2, rule3 ]
|> List.indexedMap Rule.withUniqueId
You should not have to use this when writing a rule.
-}
withRuleId : Int -> Rule -> Rule
withRuleId id (Rule rule) =
Rule { rule | id = id }
{-| Get the configuration error for a rule.
You should not have to use this when writing a rule. You might be looking for [`configurationError`](#configurationError) instead.
-}
getConfigurationError : Rule -> Maybe { message : String, details : List String }
getConfigurationError (Rule rule) =
case rule.ruleProjectVisitor of
Ok _ ->
Nothing
Err err ->
Just err
{-| **@deprecated**
This is used in [`withDeclarationVisitor`](#withDeclarationVisitor) and [`withDeclarationVisitor`](#withDeclarationVisitor),
which are deprecated and will be removed in the next major version. This type will be removed along with them.
To replicate the same behavior, take a look at
- [`withDeclarationEnterVisitor`](#withDeclarationEnterVisitor) and [`withDeclarationExitVisitor`](#withDeclarationExitVisitor).
- [`withExpressionEnterVisitor`](#withExpressionEnterVisitor) and [`withExpressionExitVisitor`](#withExpressionExitVisitor).
**/@deprecated**
Represents whether a node is being traversed before having seen its children (`OnEnter`ing the node), or after (`OnExit`ing the node).
When visiting the AST, declaration and expression nodes are visited twice: once
with `OnEnter`, before the children of the node are visited, and once with
`OnExit`, after the children of the node have been visited.
In most cases, you'll only want to handle the `OnEnter` case, but there are cases
where you'll want to visit a [`Node`](https://package.elm-lang.org/packages/stil4m/elm-syntax/7.2.1/Elm-Syntax-Node#Node)
after having seen its children.
For instance, if you are trying to detect the unused variables defined inside
of a let expression, you will want to collect the declaration of variables,
note which ones are used, and at the end of the block report the ones that weren't used.
expressionVisitor : Node Expression -> Direction -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
( [], markVariableAsUsed context name )
-- Find variables declared in let expression
( Rule.OnEnter, Expression.LetExpression letBlock ) ->
( [], registerVariables context letBlock )
-- When exiting the let expression, report the variables that were not used.
( Rule.OnExit, Expression.LetExpression _ ) ->
( unusedVariables context |> List.map createError, context )
_ ->
( [], context )
-}
type Direction
= OnEnter
| OnExit
{-| Creates a schema for a module rule. Will require adding module visitors
calling [`fromModuleRuleSchema`](#fromModuleRuleSchema) to create a usable
[`Rule`](#Rule). Use "with\*" functions from this module, like
[`withSimpleExpressionVisitor`](#withSimpleExpressionVisitor) or [`withSimpleImportVisitor`](#withSimpleImportVisitor)
to make it report something.
The first argument is the rule name. I _highly_ recommend naming it just like the
module name (including all the `.` there may be).
The second argument is the initial `moduleContext`, i.e. the data that the rule will
accumulate as the module will be traversed, and allows the rule to know/remember
what happens in other parts of the module. If you don't need a context, I
recommend specifying `()`, and using functions from this module with names
starting with "withSimple".
**NOTE**: Do not store functions, JSON values or regular expressions in your contexts, as they will be
compared internally, which [may cause Elm to crash](https://package.elm-lang.org/packages/elm/core/latest/Basics#==).
module My.Rule.Name exposing (rule)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "My.Rule.Name" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
If you do need information from other parts of the module, then you should specify
an initial context, and I recommend using "with\*" functions without "Simple" in
their name, like [`withExpressionEnterVisitor`](#withExpressionEnterVisitor),
[`withImportVisitor`](#withImportVisitor) or [`withFinalModuleEvaluation`](#withFinalModuleEvaluation).
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnusedVariables" initialContext
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
{ declaredVariables : List String
, usedVariables : List String
}
initialContext : Context
initialContext =
{ declaredVariables = [], usedVariables = [] }
-}