Coco Tidy

The Coco Platform provides a linter, called Coco Tidy, which generates warnings about recommended coding styles for programs written in Coco. This section summarises the range of checks that are provided, and how to configure them.

The checks are configured by adding the following table to the Coco.toml file:

package setting tidy
Type:Table

Specifies the configuration settings for the Coco Tidy checks that apply to the Coco source files.

By default, Coco Tidy is disabled. The enabled checks can be set using the following setting:

package setting tidy.enabled
Type:Array(String)

Comma-separated list of checks to be applied on the Coco source files belonging to the package.

The checks to be enabled can be listed individually, for example:

[tidy]
enabled = ["ReadabilityElseAfterControlFlow"]

They can also be listed under a common prefix setting. For example, the following setting enables all checks starting with Readability:

[tidy]
enabled = ["Readability"]

The string "*" is used to denote the inclusion of all checks, written as follows:

[tidy]
enabled = ["*"]

The clauses in the enabled array are evaluated from left-to-right and can also be negated, as illustrated in the following example:

[tidy]
enabled = ["*", "-ReadabilityNaming"]

This enables all checks except those starting with ReadabilityNaming.

See also

The @untidy attribute allows individual warnings to be suppressed, for example:

@untidy(.ReadabilityNaming)
function ILLEGAL_CASING() : Nil = {}

The warning will be suppressed even in the event that the function name above does not satisfy the naming convention specified.

ReadabilityBoolLikeEnum

Coco Tidy provides a check that identifies enums that are potentially being used as a booleans. In these cases, Coco Tidy will suggest using a Bool instead, thereby simplifying the code and making it more readable. This check only applies to simple enums declarations with two cases.

This check is called "ReadabilityBoolLikeEnum", and can be referenced in the Coco Tidy settings in a Coco.toml file in the same way as the other checks are, for example:

[tidy]
enabled = ["ReadabilityBoolLikeEnum"]

The following example illustrates three enum declarations, all of which would be caught under this check:

enum LikeBool1 {
  case Yes
  case No
}

enum LikeBool2 {
  case True
  case False
}

enum LikeBool3 {
  case Ok
  case NOk
}

In this example, each enum has two cases with names that would suggest they are being used as booleans.

In contrast, consider the following examples:

enum NotLikeBool1 {
  case Ok(x : Bool)
  case Nok(x : Bool)
}

enum NotLikeBool2 {
  case Blue
  case Red
}

The first example above would not be caught under this check, as it is a tagged enum. The second example would not raise a warning either, since the names of the cases are not indicative of being used as a boolean.

ReadabilityCognitiveComplexity

Coco Tidy attempts to identify code that is overly complex by evaluating the code’s cognitive complexity. This metric is calculated by analysing each Coco function, and adding a penalty for each construct that is considered to be complex. Further, the penalty is proportional to the nesting depth of the identified construct. If the total penalty in a given function exceeds a user-configurable threshold, then a warning is given.

The cognitive complexity check is configured using the following settings in the Coco.toml file of a package or workspace:

package setting tidy.ReadabilityCognitiveComplexity
Type:Table

Specifies the settings for the cognitive complexity metric to be enforced across all Coco source files in the corresponding package.

package setting tidy.ReadabilityCognitiveComplexity.threshold
Type:Int
Default value:10

Specifies the metric threshold, which leads to a warning being raised in the event a function exceeds it.

The following example illustrates how to explicitly enable this cognitive complexity check in the Coco.toml file:

[tidy]
enabled = ["ReadabilityCognitiveComplexity"]

Customising the threshold is illustrated in the following example:

[tidy.ReadabilityCognitiveComplexity]
threshold = 15

An example of a construct that contributes to the cognitive complexity metric is the if expression, and the depth of nested if-then-else expressions within. It is common for code to become more difficult to read and understand, as the nesting depth increases. Consider the following example:

function ifThenElseChain(i : Int) : Nil = {
  if (i == 0) {
  } else if (i == 1) {
  } else if (i == 2) {
  } else if (i == 3) {
  } else if (i == 4) {
  }
}

In this example, there is a chain of 5 if-then-else expressions. Each if-then-else expression in the chain is given the value of its depth relative to the outermost one, which has a value of 1, (i.e. the most nested one in this example has the value 5). The cognitive complexity score is then the sum of these values in the chain, namely 15.

In contrast, consider the following example where the function above is rewritten using a match expression:

function matchChain(i : Int) : Nil = {
  match (i) {
    0 => {},
    1 => {},
    2 => {},
    3 => {},
    4 => {},
  }
}

In this example, the depth of the match is 1, regardless of how many clauses it has. It therefore has a cognitive complexity score of 1.

Nested boolean operators also contribute towards the cognitive complexity metric, as they can rapidly contribute towards the complexity of the code, and make it more difficult to understand.

Consider the following example:

function simpleBooleanOperation(i : Int, j : Int, k : Int) : Nil = {
  if (i == 0 && j == 1 && k == 2) {
  }
}

In this example the same boolean operator is used in the condition of the if expression, and only contributes a value 1 towards the overall complexity score in this function. Together with the if expression, the total score for this function is therefore 2.

Boolean operators that are the same only increase the cognitive complexity score by a constant, as there are no additional nesting levels created. In contrast, consider the following example:

function complexBooleanOperation(i : Int, j : Int, k : Int) : Nil = {
  if ((i == 0 || j == 1) && (i == 2 || k == 3 || j == 4)) {
  }
}

In this example, there is a mix of boolean operators in the if condition, thereby creating additional nesting levels. Each nested boolean operator is assigned the value of its depth, relative to the topmost operator, which is assigned the value 1, and the overall score is the sum of them all. In this example, the complexity score is 6.

Other constructs that contribute to the cognitive complexity metric are loops, namely while and for loops, which can have further loops or other nested constructs within them. Functions that can have nested recursive calls to other functions are also examples of patterns that will contribute to the overall complexity score.

ReadabilityElseAfterControlFlow

Coco Tidy provides a check for improving the readability of if expressions by removing the else-clauses under certain circumstances. This check is called "ReadabilityElseAfterControlFlow", and can be referenced in the Coco Tidy settings in a Coco.toml file in the same way as the other checks are, for example:

[tidy]
enabled = ["ReadabilityElseAfterControlFlow"]

This check will identify an if expression with a then-clause that are guaranteed to branch, and an else-clause. In this case, the check will highlight the fact that else-clause is not required and suggest removing it.

The following function is an example of one that would be caught by this check:

function fn1(v : Bool) : Bool = {
  if (v) {
    return false;
  } else {
    return true;
  }
}

In this example, the then-clause is guaranteed to return, and therefore the check will suggest rewriting this expression without the else-clause to the following equivalent form:

function fn2(v : Bool) : Bool = {
  if (v) {
    return false;
  }
  return false;
}

The following example would also be flagged under this check, since the then-clause of the if expression is guaranteed to branch due the break statement:

function fn3() : Nil = {
  for i in range(0, 2) {
    if (i == 1) {
      break;
    } else {
    }
  }
}

ReadabilityNaming

It is common for Coco projects to have naming standards in order to improve readability, particularly across large projects with multiple users. For example, a project may have a naming convention that requires the names of all implementation components to be upper camel case and end in Impl.

With Coco Tidy, these standards can be customised and enforced by specifying them in the Coco.toml file using the settings summarised below.

package setting tidy.ReadabilityNaming
Type:Table

Specifies the settings for the naming standard to be enforced across the Coco source files in the corresponding package.

Naming conventions can be customised for a broad range of different categories of names, such as the names for attributes, enums, components, and fields. The full list of name categories can be found below. For each one, written as tidy.ReadabilityNaming.X, where X represents a name category, the naming standard is defined using any of the following three settings:

package setting tidy.ReadabilityNaming.X.casing
Type:

String

Values:
  • "CapsUpperUnderscore" – Restricts the naming convention to a combination of uppercasing of letters and the use of underscores to denote spacing between words. For example, THIS_IS_CAPS_UPPERCASE_UNDERSCORE.
  • "LowerCamelCase" – Restricts the naming convention to starting with a lowercase letter, followed by a mix of upper- and lowercasing, where an uppercase letter denotes the start of a new word. For example, thisIsLowerCamelCase.
  • "LowerUnderscore" – Restricts the naming convention to a combination of lowercasing of letters and the use of underscores to denote spacing between words. For example, this_is_lower_underscore.
  • "UpperCamelCase" – Restricts the naming convention to starting with an uppercase letter, following by a mix of upper- and lowercasing, where an uppercase letter denotes the start of a new word. For example, ThisIsUpperCamelCase.
  • "UpperUnderscore" – Restricts the naming convention to starting with an uppercase letter, following by a mix of upper- and lowercasing, and the use of an underscore to denote the start of a new word. For example, This_Is_Upper_Underscore.
  • "TypeName" – Restricts the naming convention to match that of its corresponding type name. This is typically used in conjunction with a defined suffix or prefix rule. For example, if applied to a variable declaration of type MaxSize, then this setting would restrict the variable name to MaxSize. When combined with the rule that all variables have to be prefixed with the string "v", then this variable name would have to be vMaxSize.
package setting tidy.ReadabilityNaming.X.prefix
Type:String

Restricts the naming convention for X to start with the string value specified.

package setting tidy.ReadabilityNaming.X.suffix
Type:String

Restricts the naming convention for X to end with the string value specified.

The following example illustrates a simple naming convention being applied to external and implementation components, as well as to the provided and required port fields:

[tidy.ReadabilityNaming]
externalComponents = { suffix = "Base" }
implementationComponents = { suffix = "Impl" }
providedPortFields = { casing = "TypeName", prefix = "p" }
requiredPortFields = { casing = "TypeName", prefix = "r" }

In this example, the names given to all external and implementation components must end with the suffix Base and Impl respectively. For example, an implementation component declared with the name SensorImpl, and an external component called LightBase would both satisfy this naming convention.

Further, the variable names used in the field declarations for the provided and required ports must be their type name prefixed by p and r respectively. For example, the following declarations:

val pSensor : Provided<Sensor>
val rHAL : Required<HAL>

satisfy these naming requirements.

In the event that a name category has not been customised with its own setting, name settings are automatically inherited from other name categories that have been enabled.

The following list of tables below summarises the full range of name categories that can each be customised with the three settings above, together with the settings inherited for each one. In the event there are multiple settings they can inherit, then the settings are prioritised from left to right. For example, for attributes, it can inherit from functions, and values. This means that the settings specified in the attributes table will be used, and only if this is not set, then the settings for functions will be used, and only if this one is not set either, then the settings for value will be used. Some name categories have default settings (denoted by Default value in the corresponding tables below), which are used in the event that they are enabled, but there are no settings or inherited settings specified.

package setting tidy.ReadabilityNaming.attributes
Type:Table

If not set, then it will inherit settings from (in descending order of priority): functions, values.

package setting tidy.ReadabilityNaming.components
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.componentFields
Type:Table

If not set, then it will inherit settings for (in descending order of priority): fields, values.

package setting tidy.ReadabilityNaming.encapsulatingComponents
Type:Table

If not set, then it will inherit settings for (in descending order of priority): components, types.

package setting tidy.ReadabilityNaming.enums
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.enumCases
Type:Table
Default value:casing = “UpperCamelCase”

If not set, then it will inherit settings for values.

package setting tidy.ReadabilityNaming.externalComponents
Type:Table
Default value:suffix = “Base”

If not set, then it will inherit settings for (in descending order of priority): components, types.

package setting tidy.ReadabilityNaming.externalComponentFields
Type:Table

If not set, then it will inherit settings for (in descending order of priority): componentFields, fields, values.

package setting tidy.ReadabilityNaming.externalImplementationComponents
Type:Table
Default value:suffix = “Impl”

If not set, then it will inherit settings for (in descending order of priority): components, types.

package setting tidy.ReadabilityNaming.externalTypes
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.fields
Type:Table

If not set, then it will inherit settings for values.

package setting tidy.ReadabilityNaming.functions
Type:Table

If not set, then it will inherit settings for values.

package setting tidy.ReadabilityNaming.implementationComponents
Type:Table
Default value:suffix = “Impl”

If not set, then it will inherit settings for (in descending order of priority): components, types.

package setting tidy.ReadabilityNaming.modules
Type:Table
Default value:casing = “UpperCamelCase”

If not set, then it will inherit settings for values.

package setting tidy.ReadabilityNaming.parameters
Type:Table

If not set, then it will inherit settings for (in descending order of priority): variables, values.

package setting tidy.ReadabilityNaming.ports
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.providedPortFields
Type:Table

If not set, then it will inherit settings for (in descending order of priority): fields, values.

package setting tidy.ReadabilityNaming.requiredPortFields
Type:Table

If not set, then it will inherit settings for (in descending order of priority): fields, values.

package setting tidy.ReadabilityNaming.signals
Type:Table

If not set, then it will inherit settings for (in descending order of priority): functions, values.

package setting tidy.ReadabilityNaming.states
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.structs
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.staticConstants
Type:Table
Default value:casing = “UpperCamelCase”

If not set, then it will inherit settings for (in descending order of priority): variables, types.

package setting tidy.ReadabilityNaming.traces
Type:Table

If not set, then it will inherit settings for (in descending order of priority): functions, values.

package setting tidy.ReadabilityNaming.traits
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.typeParameters
Type:Table

If not set, then it will inherit settings for types.

package setting tidy.ReadabilityNaming.types
Type:Table
Default value:casing = “UpperCamelCase”

It does not inherit settings from any other name categories.

package setting tidy.ReadabilityNaming.values
Type:Table
Default value:casing = “LowerCamelCase”

It does not inherit settings from any other name categories.

package setting tidy.ReadabilityNaming.variables
Type:Table

If not set, then it will inherit settings for values.

ReadabilityOfferIf

Coco Tidy provides a check for improving the readability of offer handlers under certain circumstances. In particular, this check will identify an offer handler of the form:

offer {
  if (g) e1,
  otherwise e2,
}

where g, e1 and e2 are expressions, and suggest it is rewritten as:

if (g) e1 else e2

which is equivalent to the offer representation above, but more readable. The fact that e1 and e2 have to be expressions means that this check intentionally does not apply in the case where either of them are illegal handlers.

This check is configured using the following settings in the Coco.toml file:

package setting tidy.ReadabilityOfferIf
Type:Table

Specifies the settings for the ReadabilityOfferIf to be enforced across all Coco source files in the corresponding package.

package setting tidy.ReadabilityOfferIf.convertToBlock
Type:Bool
Default value:false

Specifies whether the proposed if expression should be converted to a block expression. If set to true, then the if expression will be represented within a block; otherwise, it will remain as an if expression only.

This check can be referenced in the Coco Tidy settings in a Coco.toml file in the same way as the other checks are, for example:

[tidy]
enabled = ["ReadabilityOfferIf"]

The following example would be flagged under this check:

port P3 {
  function fn(x : Bool) : Bool
  machine {
    fn(x) = offer {
      if (x) true,
      otherwise false,
    }
  }
}

In this case, this check will suggest improving the readability by rewriting the offer as follows:

port P4 {
  function fn(x : Bool) : Bool
  machine {
    fn(x) = if (x) true else false
  }
}

If the convertToBlock setting is set to true as follows:

[tidy.ReadabilityOfferIf]
convertToBlock = true

then this check would propose rewriting the offer in P3 above as follows:

port P5 {
  function fn(x : Bool) : Bool
  machine {
    fn(x) = {
      if (x) true else false
    }
  }
}

ReadabilityPortOfferNondet

Coco port state machines can use both offer handlers and nondet expressions, and in certain cases, behaviour expressed using one of these constructs can be equivalently expressed using the other. When an offer handler can be represented as an equivalent nondet expression, the latter typically makes the code easier to read and understand. Coco Tidy provides a check for identifying these cases, and suggests they are expressed using a nondet instead.

This check is called "ReadabilityPortOfferNondet", and can be referenced in the Coco Tidy settings in a Coco.toml file in the same way as the other checks are, for example:

[tidy]
enabled = ["ReadabilityPortOfferNondet"]

The following function is an example of one that would be flagged under this check:

port P1 {
  function fn() : Bool
  machine {
    fn() = offer {
      true,
      false,
    }
  }
}

In this example, the offer handler will nondeterministically select one of its two clauses to execute. This is equivalent to the following example, where the offer is replaced by a nondet expression:

port P2 {
  function fn() : Bool
  machine {
    fn() = nondet {
      true,
      false,
    }
  }
}

This check only raises a warning in cases where an offer can be expressed in a equivalent form using a nondet, and therefore excludes an offer that either has guarded clauses or an otherwise clause.