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:
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:
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:
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:
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:
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:
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:
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
orprefix
rule. For example, if applied to a variable declaration of typeMaxSize
, then this setting would restrict the variable name toMaxSize
. When combined with the rule that all variables have to be prefixed with the string"v"
, then this variable name would have to bevMaxSize
.
- "CapsUpperUnderscore" – Restricts the naming convention to a combination of uppercasing of letters and the use of underscores
to denote spacing between words. For example,
-
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:
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 ablock
expression. If set totrue
, then theif
expression will be represented within ablock
; otherwise, it will remain as anif
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:
In this case, this check will suggest improving the readability by rewriting the offer
as follows:
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:
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.