/
ReviewLinksAndSections.elm
814 lines (616 loc) · 25 KB
/
ReviewLinksAndSections.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
module Docs.ReviewLinksAndSections exposing (rule)
{-|
@docs rule
-}
import Dict exposing (Dict)
import Docs.Utils.ParserExtra as ParserExtra
import Docs.Utils.Slug as Slug
import Docs.Utils.SyntaxHelp as SyntaxHelp
import Elm.Module
import Elm.Project
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Documentation exposing (Documentation)
import Elm.Syntax.Exposing as Exposing
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.Range as Range exposing (Location, Range)
import Regex exposing (Regex)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
{-| Reports problems with links and sections in Elm projects.
config =
[ Docs.ReviewLinksAndSections.rule
]
## Fail
Links to missing modules or sections are reported.
{-| Link to [missing module](Unknown-Module).
-}
a =
1
{-| Link to [missing section](#unknown).
-}
a =
1
In packages, links that would appear in the public documentation and that link to sections not part of the public documentation are reported.
module Exposed exposing (a)
import Internal
{-| Link to [internal details](Internal#section).
-}
a =
1
Sections that would have the same generated id are reported,
so that links don't inadvertently point to the wrong location.
module A exposing (element, section)
{-|
# Section
The above conflicts with the id generated
for the `section` value.
-}
element =
1
section =
1
## Success
module Exposed exposing (a, b)
import Internal
{-| Link to [exposed b](#b).
-}
a =
1
b =
2
## When (not) to enable this rule
For packages, this rule will be useful to prevent having dead links in the package documentation.
For applications, this rule will be useful if you have the habit of writing documentation the way you do in Elm packages,
and want to prevent it from going out of date.
This rule will not be useful if your project is an application and no-one in the team has the habit of writing
package-like documentation.
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/elm-review-documentation/example --rules Docs.ReviewLinksAndSections
```
## Thanks
Thanks to @lue-bird for helping out with this rule.
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "Docs.ReviewLinksAndSections" initialProjectContext
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withReadmeProjectVisitor readmeVisitor
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withFinalProjectEvaluation finalEvaluation
|> Rule.fromProjectRuleSchema
type alias ProjectContext =
{ fileLinksAndSections : List FileLinksAndSections
, isApplication : Bool
, exposedModules : Set ModuleName
}
initialProjectContext : ProjectContext
initialProjectContext =
{ fileLinksAndSections = []
, isApplication = True
, exposedModules = Set.empty
}
type alias FileLinksAndSections =
{ moduleName : ModuleName
, fileKey : FileKey
, sections : List Section
, links : List MaybeExposedLink
}
type FileKey
= ModuleKey Rule.ModuleKey
| ReadmeKey Rule.ReadmeKey
type alias ModuleContext =
{ isModuleExposed : Bool
, exposedElements : Set String
, moduleName : ModuleName
, commentSections : List SectionWithRange
, sections : List Section
, links : List MaybeExposedLink
}
type alias Section =
{ slug : String
, isExposed : Bool
}
type MaybeExposedLink
= MaybeExposedLink
{ link : SyntaxHelp.Link
, linkRange : Range
, isExposed : Bool
}
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
Rule.initContextCreator
(\metadata projectContext ->
let
moduleName : ModuleName
moduleName =
Rule.moduleNameFromMetadata metadata
in
{ isModuleExposed = Set.member moduleName projectContext.exposedModules
, exposedElements = Set.empty
, moduleName = moduleName
, commentSections = []
, sections = []
, links = []
}
)
|> Rule.withMetadata
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\moduleKey moduleContext ->
{ fileLinksAndSections =
[ { moduleName = moduleContext.moduleName
, fileKey = ModuleKey moduleKey
, sections = moduleContext.sections
, links = moduleContext.links
}
]
, isApplication = True
, exposedModules = Set.empty
}
)
|> Rule.withModuleKey
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ fileLinksAndSections = List.append newContext.fileLinksAndSections previousContext.fileLinksAndSections
, isApplication = previousContext.isApplication
, exposedModules = previousContext.exposedModules
}
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withCommentsVisitor commentsVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
-- ELM.JSON VISITOR
elmJsonVisitor : Maybe { a | project : Elm.Project.Project } -> ProjectContext -> ( List nothing, ProjectContext )
elmJsonVisitor maybeElmJson projectContext =
case Maybe.map .project maybeElmJson of
Just (Elm.Project.Package { exposed }) ->
( [], { projectContext | isApplication = False, exposedModules = listExposedModules exposed } )
_ ->
( [], projectContext )
listExposedModules : Elm.Project.Exposed -> Set ModuleName
listExposedModules exposed =
let
exposedModules : List ModuleName
exposedModules =
exposedModulesFromPackageAsList exposed
|> List.map (Elm.Module.toString >> String.split ".")
in
Set.fromList ([] :: exposedModules)
exposedModulesFromPackageAsList : Elm.Project.Exposed -> List Elm.Module.Name
exposedModulesFromPackageAsList exposed =
case exposed of
Elm.Project.ExposedList list ->
list
Elm.Project.ExposedDict list ->
List.concatMap Tuple.second list
-- README VISITOR
readmeVisitor : Maybe { readmeKey : Rule.ReadmeKey, content : String } -> ProjectContext -> ( List (Rule.Error { useErrorForModule : () }), ProjectContext )
readmeVisitor maybeReadmeInfo projectContext =
case maybeReadmeInfo of
Just { readmeKey, content } ->
let
isReadmeExposed : Bool
isReadmeExposed =
Set.member [] projectContext.exposedModules
sectionsAndLinks : { titleSections : List SectionWithRange, links : List MaybeExposedLink }
sectionsAndLinks =
findSectionsAndLinks
[]
isReadmeExposed
{ content = content
, startLocation = { row = 1, column = 1 }
}
in
( duplicateSectionErrors Set.empty sectionsAndLinks.titleSections
|> List.map (Rule.errorForReadme readmeKey duplicateSectionErrorDetails)
, { fileLinksAndSections =
{ moduleName = []
, fileKey = ReadmeKey readmeKey
, sections = List.map removeRangeFromSection sectionsAndLinks.titleSections
, links = sectionsAndLinks.links
}
:: projectContext.fileLinksAndSections
, isApplication = projectContext.isApplication
, exposedModules = projectContext.exposedModules
}
)
Nothing ->
( [], projectContext )
-- MODULE DEFINITION VISITOR
moduleDefinitionVisitor : Node Module -> ModuleContext -> ( List nothing, ModuleContext )
moduleDefinitionVisitor node context =
case Module.exposingList (Node.value node) of
Exposing.All _ ->
-- We'll keep `exposedElements` empty, which will make `declarationListVisitor` fill it with the known
-- declarations.
( [], context )
Exposing.Explicit exposed ->
( [], { context | exposedElements = Set.fromList (List.map exposedName exposed) } )
exposedName : Node Exposing.TopLevelExpose -> String
exposedName node =
case Node.value node of
Exposing.InfixExpose string ->
string
Exposing.FunctionExpose string ->
string
Exposing.TypeOrAliasExpose string ->
string
Exposing.TypeExpose exposedType ->
exposedType.name
-- COMMENTS VISITOR
commentsVisitor : List (Node String) -> ModuleContext -> ( List nothing, ModuleContext )
commentsVisitor comments context =
let
docs : List (Node String)
docs =
List.filter (Node.value >> String.startsWith "{-|") comments
sectionsAndLinks : List { titleSections : List SectionWithRange, links : List MaybeExposedLink }
sectionsAndLinks =
List.map
(\doc ->
findSectionsAndLinks
context.moduleName
context.isModuleExposed
{ content = Node.value doc, startLocation = (Node.range doc).start }
)
docs
in
( []
, { isModuleExposed = context.isModuleExposed
, exposedElements = context.exposedElements
, moduleName = context.moduleName
, commentSections = List.concatMap .titleSections sectionsAndLinks
, sections =
List.append
(List.concatMap (.titleSections >> List.map removeRangeFromSection) sectionsAndLinks)
context.sections
, links = List.append (List.concatMap .links sectionsAndLinks) context.links
}
)
-- DECLARATION VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List (Rule.Error {}), ModuleContext )
declarationListVisitor declarations context =
let
exposedElements : Set String
exposedElements =
if Set.isEmpty context.exposedElements then
Set.fromList (List.filterMap nameOfDeclaration declarations)
else
context.exposedElements
knownSections : List { slug : String, isExposed : Bool }
knownSections =
List.append
(List.map (\slug -> { slug = slug, isExposed = True }) (Set.toList exposedElements))
context.sections
knownSectionSlugs : Set String
knownSectionSlugs =
knownSections
|> List.map .slug
|> Set.fromList
sectionsAndLinks : List { titleSections : List SectionWithRange, links : List MaybeExposedLink }
sectionsAndLinks =
List.map
(findSectionsAndLinksForDeclaration
context.moduleName
(if context.isModuleExposed then
exposedElements
else
Set.empty
)
)
declarations
titleSections : List SectionWithRange
titleSections =
List.concatMap .titleSections sectionsAndLinks
in
( duplicateSectionErrors exposedElements (List.append titleSections context.commentSections)
|> List.map (Rule.error duplicateSectionErrorDetails)
, { isModuleExposed = context.isModuleExposed
, exposedElements = exposedElements
, moduleName = context.moduleName
, commentSections = context.commentSections
, sections = List.append (List.map removeRangeFromSection titleSections) knownSections
, links = List.append (List.concatMap .links sectionsAndLinks) context.links
}
)
duplicateSectionErrors : Set String -> List SectionWithRange -> List Range
duplicateSectionErrors exposedElements sections =
List.foldl
(\{ slug, range } { errors, knownSections } ->
if Set.member slug knownSections then
{ errors = range :: errors
, knownSections = knownSections
}
else
{ errors = errors
, knownSections = Set.insert slug knownSections
}
)
{ errors = [], knownSections = exposedElements }
sections
|> .errors
extractSlugsFromHeadings : { content : String, startLocation : Location } -> List (Node String)
extractSlugsFromHeadings doc =
let
lineNumberOffset : Int
lineNumberOffset =
doc.startLocation.row
in
doc.content
|> String.lines
|> List.indexedMap
(\lineNumber line ->
Regex.find specialsToHash line
|> List.concatMap .submatches
|> List.filterMap identity
|> List.map
(\slug ->
Node
{ start = { row = lineNumber + lineNumberOffset, column = 1 }
, end = { row = lineNumber + lineNumberOffset, column = String.length line + 1 }
}
(Slug.toSlug slug)
)
)
|> List.concat
findPositionOfMatch : Regex.Match -> Range
findPositionOfMatch match =
Range.emptyRange
specialsToHash : Regex
specialsToHash =
"^#{1,6}\\s+(.*)$"
|> Regex.fromString
|> Maybe.withDefault Regex.never
nameOfDeclaration : Node Declaration -> Maybe String
nameOfDeclaration node =
case Node.value node of
Declaration.FunctionDeclaration { declaration } ->
declaration
|> Node.value
|> .name
|> Node.value
|> Just
Declaration.AliasDeclaration { name } ->
Just (Node.value name)
Declaration.CustomTypeDeclaration { name } ->
Just (Node.value name)
Declaration.PortDeclaration { name } ->
Just (Node.value name)
Declaration.InfixDeclaration { operator } ->
Just (Node.value operator)
Declaration.Destructuring _ _ ->
Nothing
docOfDeclaration : Declaration -> Maybe (Node Documentation)
docOfDeclaration declaration =
case declaration of
Declaration.FunctionDeclaration { documentation } ->
documentation
Declaration.AliasDeclaration { documentation } ->
documentation
Declaration.CustomTypeDeclaration { documentation } ->
documentation
Declaration.PortDeclaration _ ->
Nothing
Declaration.InfixDeclaration _ ->
Nothing
Declaration.Destructuring _ _ ->
Nothing
findSectionsAndLinksForDeclaration : ModuleName -> Set String -> Node Declaration -> { titleSections : List SectionWithRange, links : List MaybeExposedLink }
findSectionsAndLinksForDeclaration currentModuleName exposedElements declaration =
case docOfDeclaration (Node.value declaration) of
Just doc ->
let
name : String
name =
nameOfDeclaration declaration
|> Maybe.withDefault ""
isExposed : Bool
isExposed =
Set.member name exposedElements
in
findSectionsAndLinks
currentModuleName
isExposed
{ content = Node.value doc, startLocation = (Node.range doc).start }
Nothing ->
{ titleSections = [], links = [] }
type alias SectionWithRange =
{ slug : String
, range : Range
, isExposed : Bool
}
removeRangeFromSection : SectionWithRange -> Section
removeRangeFromSection { slug, isExposed } =
{ slug = slug
, isExposed = isExposed
}
findSectionsAndLinks : ModuleName -> Bool -> { content : String, startLocation : Location } -> { titleSections : List SectionWithRange, links : List MaybeExposedLink }
findSectionsAndLinks currentModuleName isExposed doc =
let
titleSections : List SectionWithRange
titleSections =
extractSlugsFromHeadings doc
|> List.map
(\slug ->
{ slug = Node.value slug
, range = Node.range slug
, isExposed = isExposed
}
)
links : List MaybeExposedLink
links =
linksIn currentModuleName doc.startLocation doc.content
|> List.map
(\link ->
MaybeExposedLink
{ link = Node.value link
, linkRange = Node.range link
, isExposed = isExposed
}
)
in
{ titleSections = titleSections
, links = links
}
linksIn : ModuleName -> Location -> Documentation -> List (Node SyntaxHelp.Link)
linksIn currentModuleName offset documentation =
documentation
|> String.lines
|> List.indexedMap Tuple.pair
|> List.map
(\( lineNumber, lineContent ) ->
( lineNumber
, List.filterMap identity (ParserExtra.find SyntaxHelp.linkParser lineContent)
)
)
|> List.concatMap (\( lineNumber, links ) -> List.map (normalizeModuleName currentModuleName >> addOffset offset lineNumber) links)
normalizeModuleName : ModuleName -> Node SyntaxHelp.Link -> Node SyntaxHelp.Link
normalizeModuleName currentModuleName ((Node range link) as node) =
case link.file of
SyntaxHelp.ModuleTarget [] ->
Node range { link | file = SyntaxHelp.ModuleTarget currentModuleName }
SyntaxHelp.ModuleTarget _ ->
node
SyntaxHelp.ReadmeTarget ->
node
SyntaxHelp.External _ ->
node
addOffset : Location -> Int -> Node a -> Node a
addOffset offset lineNumber (Node range a) =
Node (SyntaxHelp.addOffset offset lineNumber range) a
-- FINAL EVALUATION
finalEvaluation : ProjectContext -> List (Rule.Error { useErrorForModule : () })
finalEvaluation projectContext =
let
sectionsPerModule : Dict ModuleName (List Section)
sectionsPerModule =
projectContext.fileLinksAndSections
|> List.map (\module_ -> ( module_.moduleName, module_.sections ))
|> Dict.fromList
in
List.concatMap (errorsForFile projectContext sectionsPerModule) projectContext.fileLinksAndSections
errorsForFile : ProjectContext -> Dict ModuleName (List Section) -> FileLinksAndSections -> List (Rule.Error scope)
errorsForFile projectContext sectionsPerModule fileLinksAndSections =
List.filterMap
(errorForFile projectContext sectionsPerModule fileLinksAndSections)
fileLinksAndSections.links
errorForFile : ProjectContext -> Dict ModuleName (List Section) -> FileLinksAndSections -> MaybeExposedLink -> Maybe (Rule.Error scope)
errorForFile projectContext sectionsPerModule fileLinksAndSections (MaybeExposedLink { link, linkRange, isExposed }) =
case link.file of
SyntaxHelp.ModuleTarget moduleName ->
case Dict.get moduleName sectionsPerModule of
Just existingSections ->
if Set.member fileLinksAndSections.moduleName projectContext.exposedModules && not (Set.member moduleName projectContext.exposedModules) then
Just (reportLinkToNonExposedModule fileLinksAndSections.fileKey linkRange)
else
reportIfMissingSection fileLinksAndSections.fileKey existingSections isExposed linkRange link
Nothing ->
Just (reportUnknownModule fileLinksAndSections.fileKey moduleName linkRange)
SyntaxHelp.ReadmeTarget ->
case Dict.get [] sectionsPerModule of
Just existingSections ->
reportIfMissingSection fileLinksAndSections.fileKey existingSections isExposed linkRange link
Nothing ->
Just (reportLinkToMissingReadme fileLinksAndSections.fileKey linkRange)
SyntaxHelp.External target ->
reportErrorsForExternalTarget projectContext.isApplication fileLinksAndSections.fileKey linkRange target
reportErrorsForExternalTarget : Bool -> FileKey -> Range -> String -> Maybe (Rule.Error scope)
reportErrorsForExternalTarget isApplication fileKey linkRange target =
if isApplication || String.contains "://" target then
Nothing
else
Just (reportLinkToExternalResourceWithoutProtocol fileKey linkRange)
reportIfMissingSection : FileKey -> List Section -> Bool -> Range -> SyntaxHelp.Link -> Maybe (Rule.Error scope)
reportIfMissingSection fileKey existingSectionsForTargetFile isExposed linkRange link =
case link.slug of
Just slug ->
case find (\section -> section.slug == slug) existingSectionsForTargetFile of
Just section ->
if isExposed && not section.isExposed then
Just (reportLinkToNonExposedSection fileKey linkRange)
else
Nothing
Nothing ->
Just (reportLink fileKey linkRange)
Nothing ->
Nothing
reportLink : FileKey -> Range -> Rule.Error scope
reportLink fileKey range =
reportForFile fileKey
{ message = "Link points to a non-existing section or element"
, details = [ "This is a dead link." ]
}
range
reportLinkToNonExposedModule : FileKey -> Range -> Rule.Error scope
reportLinkToNonExposedModule fileKey range =
reportForFile fileKey
{ message = "Link in public documentation points to non-exposed module"
, details = [ "Users will not be able to follow the link." ]
}
range
reportLinkToNonExposedSection : FileKey -> Range -> Rule.Error scope
reportLinkToNonExposedSection fileKey range =
reportForFile fileKey
{ message = "Link in public documentation points to non-exposed section"
, details = [ "Users will not be able to follow the link." ]
}
range
reportUnknownModule : FileKey -> ModuleName -> Range -> Rule.Error scope
reportUnknownModule fileKey moduleName range =
reportForFile fileKey
{ message = "Link points to non-existing module " ++ String.join "." moduleName
, details = [ "This is a dead link." ]
}
range
reportLinkToMissingReadme : FileKey -> Range -> Rule.Error scope
reportLinkToMissingReadme fileKey range =
reportForFile fileKey
{ message = "Link points to missing README"
, details = [ "elm-review only looks for a 'README.md' located next to your 'elm.json'. Maybe it's positioned elsewhere or named differently?" ]
}
range
reportLinkToExternalResourceWithoutProtocol : FileKey -> Range -> Rule.Error scope
reportLinkToExternalResourceWithoutProtocol fileKey range =
reportForFile fileKey
{ message = "Link to unknown resource without a protocol"
, details =
[ "I have trouble figuring out what kind of resource is linked here."
, "If it should link to a module, then they should be in the form 'Some-Module-Name'."
, "If it's a link to an external resource, they should start with a protocol, like `https://www.fruits.com`, otherwise the link will point to an unknown resource on package.elm-lang.org."
]
}
range
duplicateSectionErrorDetails : { message : String, details : List String }
duplicateSectionErrorDetails =
{ message = "Duplicate section"
, details = [ "There are multiple sections that will result in the same id, meaning that links may point towards the wrong element." ]
}
reportForFile : FileKey -> { message : String, details : List String } -> Range -> Rule.Error scope
reportForFile fileKey =
case fileKey of
ModuleKey moduleKey ->
Rule.errorForModule moduleKey
ReadmeKey readmeKey ->
Rule.errorForReadme readmeKey
find : (a -> Bool) -> List a -> Maybe a
find predicate list =
case list of
[] ->
Nothing
first :: rest ->
if predicate first then
Just first
else
find predicate rest