Skip to content

Commit 9bec3ed

Browse files
authored
Merge PR #794 from knocte/issue532-squashed-fixed
IndexerAccessorStyleConsistency: new rule.
2 parents 6e434a6 + 01a2f12 commit 9bec3ed

File tree

10 files changed

+210
-0
lines changed

10 files changed

+210
-0
lines changed

docs/content/how-tos/rule-configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,4 @@ The following rules can be specified for linting.
128128
- [EnsureTailCallDiagnosticsInRecursiveFunctions (FL0085)](rules/FL0085.html)
129129
- [FavourAsKeyword (FL0086)](rules/FL0086.html)
130130
- [InterpolatedStringWithNoSubstitution (FL0087)](rules/FL0087.html)
131+
- [IndexerAccessorStyleConsistency (FL0088)](rules/FL0088.html)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: FL0088
3+
category: how-to
4+
hide_menu: true
5+
---
6+
7+
# IndexerAccessorStyleConsistency (FL0088)
8+
9+
*Introduced in `0.26.10`*
10+
11+
## Cause
12+
13+
Use of different styles of indexer accessors in the same codebase without consistency.
14+
15+
## Rationale
16+
17+
F# 6.0 introduced a new style for indexer accessor, similar to the syntax of C#. But if your team is not
18+
convinced about this new style, you might want to make sure to stick with the previous OCaml style. Otherwise,
19+
you might want to convert all the accessors in your codebase to the new style. Consistency aides readability.
20+
21+
## How To Fix
22+
23+
If the config style is OCaml use `someArray.[0]` and if the default style is CSharp use `someArray[0]`.
24+
25+
## Rule Settings
26+
27+
{
28+
"indexerAccessorStyleConsistency": {
29+
"enabled": false,
30+
"config": {
31+
"style": "OCaml"
32+
}
33+
}
34+
}
35+
36+
* *style* - chosen style for this convention: "OCaml" or "CSharp".

src/FSharpLint.Core/Application/Configuration.fs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ open System.IO
66
open System.Reflection
77
open System.Text.Json
88
open System.Text.Json.Serialization
9+
open Microsoft.FSharp.Reflection
910
open FSharpLint.Framework
1011
open FSharpLint.Framework.Rules
1112
open FSharpLint.Framework.HintParser
@@ -17,6 +18,40 @@ let SettingsFileName = "fsharplint.json"
1718
exception ConfigurationException of string
1819

1920
module internal FSharpJsonConverter =
21+
22+
type private SimpleDUConverter<'TDiscriminatedUnion>() =
23+
inherit JsonConverter<'TDiscriminatedUnion>()
24+
25+
let allCases = FSharpType.GetUnionCases typeof<'TDiscriminatedUnion>
26+
27+
override this.Read (reader: byref<Text.Json.Utf8JsonReader>, typeToConvert: Type, options: Text.Json.JsonSerializerOptions): 'TDiscriminatedUnion =
28+
let value = reader.GetString()
29+
match allCases |> Array.tryFind (fun case -> case.Name = value) with
30+
| Some case -> FSharpValue.MakeUnion(case, Array.empty) :?> 'TDiscriminatedUnion
31+
| _ -> failwithf "Unexpected value: %A. Expected one of: %A" value allCases
32+
33+
override this.Write (writer: Text.Json.Utf8JsonWriter, value: 'TDiscriminatedUnion, options: Text.Json.JsonSerializerOptions): unit =
34+
writer.WriteStringValue(value.ToString())
35+
36+
/// JSON converter for discriminated unions that only have cases with no fields.
37+
/// Maps DUs to strings, like JsonStringEnumConverter.
38+
type SimpleDiscriminatedUnionJsonConverter() =
39+
inherit JsonConverterFactory()
40+
41+
override this.CanConvert (typeToConvert: Type): bool =
42+
FSharpType.IsUnion typeToConvert
43+
&& (FSharpType.GetUnionCases typeToConvert |> Array.forall (fun typ -> typ.GetFields().Length = 0))
44+
45+
override this.CreateConverter (typeToConvert: Type, options: JsonSerializerOptions): JsonConverter =
46+
let converterType = typedefof<SimpleDUConverter<_>>
47+
Activator.CreateInstance(
48+
converterType.MakeGenericType(Array.singleton typeToConvert),
49+
BindingFlags.Instance ||| BindingFlags.Public,
50+
binder=null,
51+
args=Array.empty,
52+
culture=null
53+
)
54+
:?> JsonConverter
2055

2156
let jsonOptions =
2257
let options =
@@ -25,6 +60,7 @@ module internal FSharpJsonConverter =
2560
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
2661
)
2762
options.Converters.Add(JsonStringEnumConverter())
63+
options.Converters.Add(SimpleDiscriminatedUnionJsonConverter())
2864
options
2965

3066
module IgnoreFiles =
@@ -300,6 +336,7 @@ with
300336
type ConventionsConfig =
301337
{ recursiveAsyncFunction:EnabledConfig option
302338
avoidTooShortNames:EnabledConfig option
339+
indexerAccessorStyleConsistency: RuleConfig<IndexerAccessorStyleConsistency.Config> option
303340
redundantNewKeyword:EnabledConfig option
304341
favourStaticEmptyFields:EnabledConfig option
305342
asyncExceptionWithoutReturn:EnabledConfig option
@@ -346,6 +383,7 @@ with
346383
this.binding |> Option.map (fun config -> config.Flatten()) |> Option.toArray |> Array.concat
347384
this.suggestUseAutoProperty |> Option.bind (constructRuleIfEnabled SuggestUseAutoProperty.rule) |> Option.toArray
348385
this.ensureTailCallDiagnosticsInRecursiveFunctions |> Option.bind (constructRuleIfEnabled EnsureTailCallDiagnosticsInRecursiveFunctions.rule) |> Option.toArray
386+
this.indexerAccessorStyleConsistency |> Option.bind (constructRuleWithConfig IndexerAccessorStyleConsistency.rule) |> Option.toArray
349387
|]
350388

351389
type TypographyConfig =
@@ -403,6 +441,7 @@ type Configuration =
403441
PatternMatchExpressionIndentation:EnabledConfig option
404442
RecursiveAsyncFunction:EnabledConfig option
405443
AvoidTooShortNames:EnabledConfig option
444+
IndexerAccessorStyleConsistency:RuleConfig<IndexerAccessorStyleConsistency.Config> option
406445
RedundantNewKeyword:EnabledConfig option
407446
FavourNonMutablePropertyInitialization:EnabledConfig option
408447
FavourReRaise:EnabledConfig option
@@ -498,6 +537,7 @@ with
498537
PatternMatchExpressionIndentation = None
499538
RecursiveAsyncFunction = None
500539
AvoidTooShortNames = None
540+
IndexerAccessorStyleConsistency = None
501541
RedundantNewKeyword = None
502542
FavourNonMutablePropertyInitialization = None
503543
FavourReRaise = None
@@ -693,6 +733,7 @@ let flattenConfig (config:Configuration) =
693733
config.PatternMatchExpressionIndentation |> Option.bind (constructRuleIfEnabled PatternMatchExpressionIndentation.rule)
694734
config.RecursiveAsyncFunction |> Option.bind (constructRuleIfEnabled RecursiveAsyncFunction.rule)
695735
config.AvoidTooShortNames |> Option.bind (constructRuleIfEnabled AvoidTooShortNames.rule)
736+
config.IndexerAccessorStyleConsistency |> Option.bind (constructRuleWithConfig IndexerAccessorStyleConsistency.rule)
696737
config.RedundantNewKeyword |> Option.bind (constructRuleIfEnabled RedundantNewKeyword.rule)
697738
config.FavourNonMutablePropertyInitialization |> Option.bind (constructRuleIfEnabled FavourNonMutablePropertyInitialization.rule)
698739
config.FavourReRaise |> Option.bind (constructRuleIfEnabled FavourReRaise.rule)

src/FSharpLint.Core/FSharpLint.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
<Compile Include="Rules\Conventions\SourceLength\MaxLinesInFile.fs" />
125125
<Compile Include="Rules\Formatting\Typography\TrailingNewLineInFile.fs" />
126126
<Compile Include="Rules\Formatting\Typography\NoTabCharacters.fs" />
127+
<Compile Include="Rules\Conventions\IndexerAccessorStyleConsistency.fs" />
127128
<Compile Include="Rules\Hints\HintsHelper.fs" />
128129
<Compile Include="Rules\Hints\HintMatcher.fs" />
129130
<!-- Application -->
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module FSharpLint.Rules.IndexerAccessorStyleConsistency
2+
3+
open FSharp.Compiler.Syntax
4+
open FSharpLint.Framework.Ast
5+
open FSharpLint.Framework.AstInfo
6+
open FSharpLint.Framework.Rules
7+
open FSharpLint.Framework.Suggestion
8+
open FSharpLint.Framework
9+
open System
10+
11+
type IndexerAccessorStyle =
12+
| OCaml
13+
| CSharp
14+
15+
[<RequireQualifiedAccess>]
16+
type Config = {
17+
Style: IndexerAccessorStyle
18+
}
19+
20+
let generateOutput (range: FSharp.Compiler.Text.Range) (style: IndexerAccessorStyle) =
21+
Array.singleton
22+
{
23+
Range = range
24+
Message = String.Format(Resources.GetString "RulesIndexerAccessorStyleConsistency", style.ToString())
25+
SuggestedFix = None
26+
TypeChecks = List.Empty
27+
}
28+
29+
let runner (config: Config) (args: AstNodeRuleParams) =
30+
match config.Style with
31+
| IndexerAccessorStyle.OCaml ->
32+
match args.AstNode with
33+
| AstNode.Expression (SynExpr.App (ExprAtomicFlag.Atomic, _, SynExpr.Ident _, SynExpr.ArrayOrListComputed (_, _expr, range), _)) ->
34+
generateOutput range IndexerAccessorStyle.OCaml
35+
| _ ->
36+
Array.empty
37+
| IndexerAccessorStyle.CSharp ->
38+
match args.AstNode with
39+
| AstNode.Expression (SynExpr.DotIndexedGet (_, _, _, range)) ->
40+
generateOutput range IndexerAccessorStyle.CSharp
41+
| _ ->
42+
Array.empty
43+
44+
let rule config =
45+
AstNodeRule
46+
{
47+
Name = "IndexerAccessorStyleConsistency"
48+
Identifier = Identifiers.IndexerAccessorStyleConsistency
49+
RuleConfig = { AstNodeRuleConfig.Runner = runner config; Cleanup = ignore }
50+
}

src/FSharpLint.Core/Rules/Identifiers.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,4 @@ let FavourNonMutablePropertyInitialization = identifier 84
9292
let EnsureTailCallDiagnosticsInRecursiveFunctions = identifier 85
9393
let FavourAsKeyword = identifier 86
9494
let InterpolatedStringWithNoSubstitution = identifier 87
95+
let IndexerAccessorStyleConsistency = identifier 88

src/FSharpLint.Core/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,7 @@
384384
<data name="InterpolatedStringWithNoSubstitution" xml:space="preserve">
385385
<value>Do not use interpolated string syntax (with $ prefix) or formatting functions (sprintf, failwithf) when not really performing any interpolation.</value>
386386
</data>
387+
<data name="RulesIndexerAccessorStyleConsistency" xml:space="preserve">
388+
<value>Consider switching the indexer accessor to {0} style.</value>
389+
</data>
387390
</root>

src/FSharpLint.Core/fsharplint.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,12 @@
334334
"ensureTailCallDiagnosticsInRecursiveFunctions": { "enabled": true },
335335
"favourAsKeyword": { "enabled": true },
336336
"interpolatedStringWithNoSubstitution": { "enabled": true },
337+
"indexerAccessorStyleConsistency": {
338+
"enabled": false,
339+
"config": {
340+
"style": "OCaml"
341+
}
342+
},
337343
"hints": {
338344
"add": [
339345
"not (a = b) ===> a <> b",

tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<Compile Include="Rules\Conventions\UsedUnderscorePrefixedElements.fs" />
4848
<Compile Include="Rules\Conventions\FavourNonMutablePropertyInitialization.fs" />
4949
<Compile Include="Rules\Conventions\EnsureTailCallDiagnosticsInRecursiveFunctions.fs" />
50+
<Compile Include="Rules\Conventions\IndexerAccessorStyleConsistency.fs" />
5051
<Compile Include="Rules\Conventions\Naming\NamingHelpers.fs" />
5152
<Compile Include="Rules\Conventions\Naming\InterfaceNames.fs" />
5253
<Compile Include="Rules\Conventions\Naming\ExceptionNames.fs" />
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module FSharpLint.Core.Tests.Rules.Conventions.IndexerAccessorStyleConsistency
2+
3+
open NUnit.Framework
4+
open FSharpLint.Rules
5+
open FSharpLint.Core.Tests
6+
open FSharpLint.Rules.IndexerAccessorStyleConsistency
7+
8+
[<TestFixture>]
9+
type TestConventionsIndexerAccessorStyleConsistencyCSharp() =
10+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(IndexerAccessorStyleConsistency.rule { Style = IndexerAccessorStyle.CSharp })
11+
12+
[<Test>]
13+
member this.IndexerAccessorStyleConsistencyOCamlStyleWhenUsingCSharp() =
14+
this.Parse """
15+
module Program
16+
17+
let someArray = [| "foo" ; "bar" |]
18+
let bar = someArray.[1]
19+
System.Console.WriteLine bar"""
20+
21+
Assert.IsTrue this.ErrorsExist
22+
23+
[<Test>]
24+
member this.IndexerAccessorStyleConsistencyCSharpStyleWhenUsingCSharp() =
25+
this.Parse """
26+
module Program
27+
28+
let someArray = [| "foo" ; "bar" |]
29+
let bar = someArray[1]
30+
System.Console.WriteLine bar"""
31+
32+
Assert.IsTrue this.NoErrorsExist
33+
34+
[<TestFixture>]
35+
type TestConventionsIndexerAccessorStyleConsistencyOCaml() =
36+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(IndexerAccessorStyleConsistency.rule { Style = IndexerAccessorStyle.OCaml })
37+
38+
[<Test>]
39+
member this.IndexerAccessorStyleConsistencyCSharpStyleWhenUsingOCaml() =
40+
this.Parse """
41+
module Program
42+
43+
let someArray = [| "foo" ; "bar" |]
44+
let bar = someArray[1]
45+
System.Console.WriteLine bar"""
46+
47+
Assert.IsTrue this.ErrorsExist
48+
49+
[<Test>]
50+
member this.IndexerAccessorStyleConsistencyCSharpStyleWhenUsingOCaml2() =
51+
this.Parse """
52+
module Program
53+
54+
let withoutPrefix = [ "foo" ]
55+
let firstChar = withoutPrefix[0] |> string
56+
let rest = withoutPrefix.Substring 1
57+
prefix + firstChar + rest"""
58+
59+
Assert.IsTrue this.ErrorsExist
60+
61+
[<Test>]
62+
member this.IndexerAccessorStyleConsistencyOCamlStyleWhenUsingOCaml() =
63+
this.Parse """
64+
module Program
65+
66+
let someArray = [| "foo" ; "bar" |]
67+
let bar = someArray.[1]
68+
System.Console.WriteLine bar"""
69+
70+
Assert.IsTrue this.NoErrorsExist

0 commit comments

Comments
 (0)