Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule ideas #2

Open
cmeeren opened this issue Dec 28, 2022 · 0 comments
Open

Rule ideas #2

cmeeren opened this issue Dec 28, 2022 · 0 comments

Comments

@cmeeren
Copy link
Owner

cmeeren commented Dec 28, 2022

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:

  • 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

  • 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

      • Avoid out/ref parameters
    • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant