This issue serves as notes for rule ideas.
In general
- Rules should not be based on personal opinion. They should be based on (and point to) official recommendations or other well-reasoned/researched sources. If you want a rule that is not covered there, try to get it added to official coding guidelines first.
- Rules should be sufficiently generally applicable that they can be enabled by default.
- Configuration options should be minimal. Like Fantomas, Flinter should do "the right thing" out of the box and push toward uniform(ly good) F# code. Flinter should improve the F# ecosystem; that is harder to do when everyone can configure everything.
Out of scope
- Code formatting (handled by Fantomas)
- Anything that is better handled by the compiler itself (raise an issue there instead)
Ideas for categories (not complete)
- Complexity (e.g., cyclomatic complexity, function/method size, number of activated entities)
- Consistency (e.g., naming)
- Correctness (e.g.,
missing use, recursive async CE via do! instead of return!, recursive task CE, missing tail recursion (or is that not generally useful?))
- Performance (e.g., use struct single-case DUs)
- Redundancy (e.g., dot in index notation, named
unit values, string applied to string argument, etc.)
- Robustness
- Security
- Some categories here
- More categories here
Rule ideas
-
Quick-notes, to be elaborated/merged into the below:
let/let! instead of use/use! on IDisposable and IAsyncDisposable (for bang operators, depending on the CE, these may be wrapped in arbitrary container types like Async or Task)
- Dead code
- Ideas from:
- Naming
- Unneeded type annotations (in internal/private code) - can this be detected?
- Using prefix notation for generics (
Foo<'a>) except for option, voption, list, [], ref (source: formatting guidelines)
- Named parameters or values that are typed to unit (e.g.
Option.defaultWith (fun thisIsUnit -> ...))
- Usage of
Option.Value, List.Head and similar?
- Recommend
Sealed and AbstractClass on types with only static members?
- Don't catch an exception and throw from the
with expression without the thrown exception containing the caught exception (loses stack trace and other information)
- Use string interpolation instead of
sprintf, failwithf etc. (when not piped)
-
Guidelines from the book "Code that fits in your head" by Mark Seemann
-
Limit number of "activated objects" in any given function (section 7.2.7 of the book)
- A method should interact with a maximum number of local variables, method parameters, and class fields.
- As a rule of thumb, try to keep this number at 7 or below. Keeping it low ensures that the method implementation fits in your head.
- What about nested functions? There's no single-level "Class -> Method" structure; functions can have arbitrarily nested inner functions (often closing over some values from the outer scope).
-
Cyclomatic complexity (section 7.1.2 of the book)
- It starts at 1 and increases by 1 for each branch. This includes not only if/elseif/else, but also ternary operators, null coalescing operators, pattern match cases, loop bodies, etc.
- Counting all pattern match cases would make it impossible to avoid this rule. A single function with a top-level pattern match against an enum with too many cases would trigger this rule, and nothing sensible could be done about it.
- As a rule of thumb, try to keep this number at 7 or below. Keeping it low ensures that the method implementation fits in your head.
-
Parse, don't validate (section section 7.2.5 of the book)
- If you just validate a string (or any other type, for that matter) and then pass that string further into your code, other parts of the code have no guarantees about what you validated, and it's hard to know at any point in the code what guarantees the string comes with. Should the deeper code validate the string again? It's impossible to know at that level; it must assume knowledge about what happens outside it.
- Instead, when you validate data, wrap it in a type that carries the guarantees you desire. Do this as soon as possible in your code (i.e., just after receiving it from the outside world), and then only work with the custom type inside your code.
As a simple example, if you receive an email address via a string in an API, once you have validated that the string is a valid email address, wrap the string in a custom EmailAddress type and use that everywhere else. All code that uses EmailAddress knows that they have a valid email address, as opposed to if they had just worked with a plain string.
- How to implement this? E.g. if passing a primitive like a string into a function, passing it to a predicate (a
bool-returning function), and then returning the string (unmodified). Might be complicated.
-
Guidelines from F# coding conventions
-
Prefer namespaces instead of modules at the top level
- Remember to count
module Foo.Bar as a namespace
- Doesn't make sense to recommend this for apps (at least if single-project)?
-
Carefully apply AutoOpen
- Trigger on AutoOpen of public modules? Only in libraries?
-
Use classes to contain values that have side effects
- Not sure how to implement this, since it's impossible to know generally which expressions have side effect. But could trigger on:
- mutable/ref module-level values
- module-level calls to functions/methods that accept or return
unit (also check in module-level values, recursively)
- This will likely also detect partial application of function that are bound as values, not functions, where a partially applied parameter is impure. For example,
let f = f' a b if a or b (any part of them if multi-part) are mutable.
- Also detect a list of well-known impure calls that are not caught by the above rules? E.g.
DateTime.Now etc.
-
Do not use monadic error handling to replace exceptions
- Trigger when constructing
Result.Error or a Choice case with an exception
- Trigger when catching an exception and returning
Error exn.Message?
- Trigger when catching all exceptions using
_ and returning None
-
Strive to keep all helper functionality private
- Trigger on public modules named
Helper, Helpers, Util, Utils, etc.?
-
Explicitly annotate all parameter and return types in public functions
- Must only check library projects, not console apps (where
OutputType is Exe)
-
Consider giving a meaningful name to your generic arguments
- Trigger on type parameters named
'a, 'b, 'T, etc. Only if public, and only in libraries, not console apps (where OutputType is Exe)
- Also see this
-
Consider naming generic type parameters with PascalCase
- Don't trigger on single-character names like
'a, 'b', etc., since that is common in F#
- Allow configuring a list of allowed non-PascalCase names?
-
Consider structs for small types with high allocation rates
- Can at least trigger on single-case DUs where the data is a single primitive type (value type < 16 bytes or reference type)
- More triggers?
-
Prefer let mutable to ref
-
Avoid the use of the AllowNullLiteral attribute
-
Avoid the use of the Unchecked.defaultof<_>
-
Avoid the use of the DefaultValue attribute
-
Avoid inheritance-based type hierarchies and implementation inheritance
-
Use object expressions to implement interfaces if you don't need a class
- Could trigger on classes that only implement interfaces and have no other members
-
Guidelines from F# component design guidelines
-
Stuff from .NET framework design guidelines
-
Naming guidelines - common capitalization mistakes
- But verify each one; some of them seem wrong, like
userName (username seems to be a closed-form compound word)
-
Names of common types
Attribute suffix for types inheriting from System.Attribute
EventArgs suffix for types inheriting from System.EventArgs
- Do not suffix enum types with
Enum, Flag, or Flags
Dictionary suffix for types implementing IDictionary
Collection suffix for types implementing IEnumerable, ICollection, or IList
Stream suffix for types inheriting from System.IO.Stream
- Use singular type names for enumerations that are not bit fields (flags), see
System.FlagsAttribute
- Use plural type names for enumerations that are bit fields (flags), see
System.FlagsAttribute
-
Member names
- Do not have a property that when prefixed with
Get has the same name as a method. This pattern typically indicates that the property should really be a method. (What about Set?)
- DO name collection properties with a plural phrase describing the items in the collection instead of using a singular phrase followed by "List" or "Collection".
- Events should not be prefixed or suffixed with
Before or After
-
Parameter names
- DO use
left and right for binary operator overload parameter names if there is no meaning to the parameters.
- Is this applicable to F#?
- DO use
value for unary operator overload parameter names if there is no meaning to the parameters.
- Is this applicable to F#?
-
Interface design
- AVOID using marker interfaces (interfaces with no members). Use an attribute instead.
- DO provide at least one type that is an implementation of an interface.
- DO provide at least one API that consumes each interface you define (a method taking the interface as a parameter or a property typed as the interface).
-
Struct design
- DO NOT provide a parameterless constructor for a struct.
- DO NOT define mutable value types.
- DO implement
IEquatable<T> on value types.
-
Enum design
- DO use powers of two for the flag enum values so they can be freely combined using the bitwise OR operation. (See
System.FlagsAttribute)
- Also do the reverse check, by detecting enums with powers-of-two values that do not have the attribute?
- What about this? "CONSIDER providing special enum values for commonly used combinations of flags."
- DO name the zero value of flag enums None. For a flag enum, the value must always mean "all flags are cleared."
-
Property design
- DO NOT provide set-only properties or properties with the setter having broader accessibility than the getter.
- AVOID throwing exceptions from property getters.
- AVOID indexers with parameter types other than
Int32, Int64, String, Object, or an enum.
-
Event design (low priority?)
-
Field design
- DO NOT provide instance fields that are public or protected.
-
Parameter design
-
Exception throwing
- DO NOT have public members that return exceptions as the return value or an out parameter.
- AVOID explicitly throwing exceptions from finally blocks.
-
**Using standard exception types
- Don't throw
Exception (failwith/failwithf) or SystemException; use a more specific exception
- But does this really matter if it's an exception that should never occur? For example, Felicity throws many exceptions from its internals that are never intended to be caught, just to supply a helpful error message.
- DO NOT catch
System.Exception or System.SystemException in framework code, unless you intend to rethrow.
- AVOID catching System.Exception or System.SystemException, except in top-level exception handlers.
- DO NOT throw or derive from
ApplicationException.
- DO NOT allow publicly callable APIs to explicitly or implicitly throw
NullReferenceException, AccessViolationException, or IndexOutOfRangeException. These exceptions are reserved and thrown by the execution engine and in most cases indicate a bug.
- DO NOT explicitly throw
StackOverflowException
- DO NOT catch
StackOverflowException.
- DO NOT explicitly throw
OutOfMemoryException
- DO NOT explicitly throw
COMException, ExecutionEngineException, and SEHException`
-
Usage guidelines: Arrays
- DO prefer using collections over arrays in public APIs
- DO NOT use read-only array fields
- This is a subset of "do not set read-only fields to mutable types" mentioned elsewhere in the framework design guidelines
-
Usage guidelines: Attributes
-
Usage guidelines: Collections
-
Officially documented best practices:
-
Check existing linter rules for ideas (note: Before implementation, they should be grounded in something more than just "implemented in another linter")
-
FSharpLint
- Recursive async functions should end with
return! not do!
- Redundant
new keyword?
failwith, raise, nulArg, invalidOp being passed more than one argument
invalidArg being passed more than two arguments
failwithf being passed more arguments than contained in the format string
- All the "max lines in ..." and "max number of ...": Are these all better covered by "cyclomatic complexity" and "max number of activated objects" (from "Code that fits in your head")?
- Simplify lambda (only if lambda re-implements function, no partial application? There are some important false positives here; see own reported issues in JetBrains issue tracker for examples)
- Replace lambda with piping/composition (see caveats above)
- Favor
ignore over let _
- Replace match clause
_ as x with x
- Replace wildcard tuples
(_, _) with wildcard _
- Simplified custom hint syntax? Use this internally as an implementation detail, too?
- Avoid partial functions like
List.find (but what it you know it will exist? Pointless to use pattern matching only to have the None case be failwith "Unreachable code")
- Favor typed ignore
- Use
reraise () instead of raise inside a try ... with block (not applicable inside a CE)
- Consistent
this identifier in classes/interfaces (is this covered by the F# style guide?)
-
Various Roslyn analyzers
TODO: For all rules that work with accessibility modifiers, what if signatures files are used?
Dev notes
This issue serves as notes for rule ideas.
In general
Out of scope
Ideas for categories (not complete)
missing, recursiveuseasyncCE viado!instead ofreturn!, recursivetaskCE, missing tail recursion (or is that not generally useful?))unitvalues,stringapplied to string argument, etc.)Rule ideas
Quick-notes, to be elaborated/merged into the below:
let/let!instead ofuse/use!onIDisposableandIAsyncDisposable(for bang operators, depending on the CE, these may be wrapped in arbitrary container types likeAsyncorTask)Foo<'a>) except foroption,voption,list,[],ref(source: formatting guidelines)Option.defaultWith (fun thisIsUnit -> ...))Option.Value,List.Headand similar?SealedandAbstractClasson types with only static members?withexpression without the thrown exception containing the caught exception (loses stack trace and other information)sprintf,failwithfetc. (when not piped)Guidelines from the book "Code that fits in your head" by Mark Seemann
Limit number of "activated objects" in any given function (section 7.2.7 of the book)
Cyclomatic complexity (section 7.1.2 of the book)
Parse, don't validate (section section 7.2.5 of the book)
As a simple example, if you receive an email address via a string in an API, once you have validated that the string is a valid email address, wrap the string in a custom
EmailAddresstype and use that everywhere else. All code that usesEmailAddressknows that they have a valid email address, as opposed to if they had just worked with a plain string.bool-returning function), and then returning the string (unmodified). Might be complicated.Guidelines from F# coding conventions
Prefer namespaces instead of modules at the top level
module Foo.Baras a namespaceCarefully apply AutoOpen
Use classes to contain values that have side effects
unit(also check in module-level values, recursively)let f = f' a bifaorb(any part of them if multi-part) are mutable.DateTime.Nowetc.Do not use monadic error handling to replace exceptions
Result.Erroror aChoicecase with an exceptionError exn.Message?_and returningNoneStrive to keep all helper functionality private
Helper,Helpers,Util,Utils, etc.?Explicitly annotate all parameter and return types in public functions
OutputTypeisExe)Consider giving a meaningful name to your generic arguments
'a,'b,'T, etc. Only if public, and only in libraries, not console apps (whereOutputTypeisExe)Consider naming generic type parameters with PascalCase
'a,'b', etc., since that is common in F#Consider structs for small types with high allocation rates
Prefer
let mutabletorefAvoid the use of the
AllowNullLiteralattributeAvoid the use of the
Unchecked.defaultof<_>Avoid the use of the
DefaultValueattributeAvoid inheritance-based type hierarchies and implementation inheritance
Use object expressions to implement interfaces if you don't need a class
Guidelines from F# component design guidelines
Use XML docs for the entire public API surface (applicable for libraries)
Follow .NET naming conventions
Use namespaces or modules to contain your types and modules
Is this only relevant for single-file projects? For multi-file projects, the compiler enforces this, right?
Use interfaces to group related operations
Hide the representations of record and union types if the design of these types is likely to evolve
Avoid the use of implementation inheritance for extensibility
Don't return long tuples
Use correct Async prefix/postfix for Async/Task overloads of sync methods
Avoid defining custom symbolic operators
Carefully use type abbreviations)
Rules from the sections relevant only when intending consumption from other .NET languages (not 1st priority)
Stuff from .NET framework design guidelines
Naming guidelines - common capitalization mistakes
userName(username seems to be a closed-form compound word)Names of common types
Attributesuffix for types inheriting fromSystem.AttributeEventArgssuffix for types inheriting fromSystem.EventArgsEnum,Flag, orFlagsDictionarysuffix for types implementingIDictionaryCollectionsuffix for types implementingIEnumerable,ICollection, orIListStreamsuffix for types inheriting fromSystem.IO.StreamSystem.FlagsAttributeSystem.FlagsAttributeMember names
Gethas the same name as a method. This pattern typically indicates that the property should really be a method. (What aboutSet?)BeforeorAfterParameter names
leftandrightfor binary operator overload parameter names if there is no meaning to the parameters.valuefor unary operator overload parameter names if there is no meaning to the parameters.Interface design
Struct design
IEquatable<T>on value types.Enum design
System.FlagsAttribute)Property design
Int32,Int64,String,Object, or an enum.Event design (low priority?)
Field design
Parameter design
Exception throwing
**Using standard exception types
Exception(failwith/failwithf) orSystemException; use a more specific exceptionSystem.ExceptionorSystem.SystemExceptionin framework code, unless you intend to rethrow.ApplicationException.NullReferenceException,AccessViolationException, orIndexOutOfRangeException. These exceptions are reserved and thrown by the execution engine and in most cases indicate a bug.StackOverflowExceptionStackOverflowException.OutOfMemoryExceptionCOMException,ExecutionEngineException, andSEHException`Usage guidelines: Arrays
Usage guidelines: Attributes
Usage guidelines: Collections
Officially documented best practices:
Check existing linter rules for ideas (note: Before implementation, they should be grounded in something more than just "implemented in another linter")
FSharpLint
return!notdo!newkeyword?failwith,raise,nulArg,invalidOpbeing passed more than one argumentinvalidArgbeing passed more than two argumentsfailwithfbeing passed more arguments than contained in the format stringignoreoverlet __ as xwithx(_, _)with wildcard_List.find(but what it you know it will exist? Pointless to use pattern matching only to have theNonecase befailwith "Unreachable code")reraise ()instead ofraiseinside atry ... withblock (not applicable inside a CE)thisidentifier in classes/interfaces (is this covered by the F# style guide?)Various Roslyn analyzers
TODO: For all rules that work with accessibility modifiers, what if signatures files are used?
Dev notes