XBSL linter rules
The full list of linter checks, with severities and scope.
The full list of linter checks. This file is extended as rules are added; the live list at
runtime is xbsl --list-rules (or the MCP list_rules). Currently there are 191 rules.
The table describes the toolkit as it ships. An installed plugin may add rules of its own and
override severities and default states (see Extending),
so the runtime list can differ from this one – xbsl --list-rules shows what your environment
actually runs, and XBSL_NO_PLUGINS=1 shows the set below.
Boundary: the linter complements the compiler, it does not replace it
The linter works over text, the AST and the project model. Its rules know types at the first
hop: the declared nominal type of a variable and its members, the project objects and the
types they generate, enumeration values, the global types of the linked libraries (from the
.xlib archive).
The engine DOES infer the type of an expression - xbsl.typeinfer answers for a receiver, a
member, a constructor, a cast and a non-null operator, and the inference of chains and locals
feeds hover and completion in the editor. The checks do not rest on it: the rules judge by the
declared types - see below on what the linter does not do.
Some of the findings the compiler would catch as well: an unknown type, an argument count, a
non-exception in catch, a return not matching the signature. The linter’s value there is
not that it sees more, but that it sees them earlier - in seconds on your own machine,
before the build and the deploy, pointing at the exact spot. The rest the compiler never
checks at all: code-writing conventions, typography, project structure (duplicate Id,
file pairing), unused variables, secrets in the sources.
What the linter does not do is anything that needs full inference of expression types: a
redundant cast, a leaked resource in the general case, whether the TYPE of a returned value
matches the signature. Two of those are worth separating. A structural return mismatch (a
value in a void method, a bare return in a typed one) is caught by code/return-mismatch,
while a return of a string from a method declared : Number slips through - telling that
apart needs the expression’s type. And a resource is judged only in the one shape where the
declaration itself says everything: code/unclosed-resource follows a closeable from its
declaration to the loop over it in the same method; a resource that travels through calls or
collections stays out of reach.
Code correctness is verified by the server-side compilation on deploy; the linter runs before it and removes common mistakes early.
How to read the table
- Rule – the
group/nameidentifier. The group (the part before/) lets you enable and disable rules in bulk. - Level –
error(a build and CI should fail),warning(a convention is broken),info(a hint, usually off). - Default – ✓ the rule is in the default set, – it is enabled explicitly.
- Scope –
file(the rule sees one file) orproject(needs the whole-project index: duplicate Ids, unknown types, cross-module calls). - The link at the end of a description – the platform documentation section behind the rule. In VS Code the code of such a rule in the Problems panel opens that section right in the editor.
Tiers
Rules are split into tiers A-D by what they rely on. A tier is also a quick filter for
--select/--ignore (alongside the group and the identifier): --select A,B runs only
structure and text, --ignore D drops the semantics over stdlib.
Reading the columns: error · warning · info; ✓ – in the default set, – turned on explicitly; the scope is one file or the whole project.
Tier A - structure and YAML
The file exists, parses, the object has a unique UUID, the name matches the file.
| Rule | Scope | What it checks | ||
|---|---|---|---|---|
yaml/valid |
✓ | file | YAML does not parse | |
yaml/duplicate-key |
✓ | file | A scalar key set twice in one YAML mapping: the loader silently keeps the last value, every schema check reads the already-merged document, and the compiler rejects the file on deploy. The second and later occurrences are flagged, naming the line of the first; the << merge key and non-scalar keys are not judged, and keys compare the way the loader tells them apart (tag and text) |
|
yaml/id-uuid |
✓ | file | Id is not a UUID | |
yaml/id-required |
✓ | file | The object has no Id | |
yaml/name-matches-file |
✓ | file | Name does not match the file name | |
yaml/id-unique |
✓ | project | Duplicate Id in the project | |
yaml/standard-field-length |
✓ | file | A standard field longer than the platform limit (Name over 400 characters, Code over 50) - apply rejects the field and it drops out of the object docs |
|
yaml/ref-needs-nullable |
✓ | file | A reference type in a type position without ? (Goods.Reference, Edit<Goods.Reference>) - a reference has no default value, the compilation fails with Default value initialization is not supported docs |
|
yaml/no-expression-in-literal |
✓ | file | An =... expression inside a literal-typed node (Font: {Type: AbsoluteFont, Size: =...}) - the platform accepts only a literal there, compute the whole object instead docs |
|
yaml/localization-key-unique |
✓ | file | A key a LocalizedStrings dictionary declares twice - Strings and Templates share one namespace, and a translation file is judged too; the apply answers “Name is not unique” and rolls the project back docs |
|
yaml/unused-component |
✓ | project | An interface component placed nowhere and created nowhere: neither as a Type value in markup nor by new in code (code/unused-method cannot see it - the component’s methods are called by its own yaml). A use is a yaml value (a name as a dictionary KEY does not count) or any word of a module. Never judged: an entry point and VisibilityScope: Global - the public surface of a library. Without a project descriptor among the linted files the rule stays silent: on a subset a component placed outside would look dead |
|
yaml/duplicate-subtree |
– | project | A markup subtree repeating the shape of a subtree in ANOTHER file (names, ids and texts are left out of the shape): a new form is started by copying the neighbouring one. The 40-node threshold is measured - below it the rule catches layout, not copies. Never judged: a repeat inside one file, the data source of a list and a localized-strings dictionary; only maximal groups are named. Off by default: how much sameness is too much is a decision of the project | |
project/identifier |
✓ | file | Project name or vendor is not an identifier docs | |
project/presentation |
✓ | file | Project presentation is empty docs | |
project/version |
✓ | file | Project version is not A.B.C docs | |
structure/xbsl-pair |
✓ | file | Module .xbsl without a paired .yaml | |
project/path-matches-descriptor |
✓ | file | The {{vendor}}/{{name}} path diverged from the descriptor – a build refuses the project before compiling docs |
|
yaml/unknown-component-property |
✓ | file | A markup key the component does not declare while ANOTHER component of the ui schema does (Checkbox + PlaceholderText, a property of Edit) - apply rejects the markup node as an unknown property; a name no component declares is left alone, the documentation does not list the yaml keys in full docs |
|
yaml/inline-command-name |
✓ | file | A Name on a command declared inline in the markup (an inline command-interface fragment or a single-command property) - the apply refuses the node (“a command name is allowed only in command-interface-fragment project elements”) and rolls back; reach the command through the handler parameter, or move the fragment into a project element of its own docs |
Tier B - text and conventions
Encoding, newlines, whitespace, typography (dashes, quotes, ellipsis), line length, secrets in the sources.
| Rule | Scope | What it checks | ||
|---|---|---|---|---|
security/hardcoded-secret |
✓ | file | A key or a password as a literal | |
typography/em-dash |
– | file | Em dash in a comment | |
typography/ellipsis |
✓ | file | Ellipsis character in a comment | |
typography/curly-quotes |
✓ | file | Curly quotes | |
typography/guillemets-comment |
– | file | Guillemets in a comment | |
typography/yo-in-text |
– | file | Letter “ё” in interface text | |
whitespace/trailing |
✓ | file | Trailing whitespace | |
whitespace/mixed-newline |
✓ | file | Mixed newlines | |
encoding/utf8 |
✓ | file | File is not UTF-8 | |
style/tab-indent |
✓ | file | Tab in the indentation docs | |
style/line-length |
✓ | file | Line longer than 120 characters docs |
Tier C - code structure, basic syntax and code-writing conventions
Block and bracket balance, loop and method headers, local variables and the style/ group -
conventions from the documentation section “Code-writing recommendations”. Some style/ rules
are off by default (accumulated debt, info): enable them with --select style to measure.
| Rule | Scope | What it checks | ||
|---|---|---|---|---|
code/parse-error |
✓ | file | Syntax error (a full parse against the platform grammar) docs | |
code/statement-no-effect |
✓ | file | Expression statement with no effect: the value is dropped (often a keyword typo, retun 5 for return 5) |
|
code/return-mismatch |
✓ | file | Return does not match the method signature (a value in a void method, a bare return in a typed one) - the compiler rejects such code docs |
|
code/call-arity |
✓ | file | Argument count of a local call outside the method’s [required, total] range docs | |
code/brackets |
✓ | file | Unbalanced brackets () [] {} | |
code/blocks |
✓ | file | Unbalanced blocks and ‘;’ docs | |
code/ternary-and-or |
✓ | file | Compound ternary condition without parentheses docs | |
code/query-in-loop |
✓ | file | A query inside a loop | |
code/param-type-required |
✓ | file | Parameter without a type and without a default value docs | |
code/duplicate-annotation |
✓ | file | Duplicate annotation on a declaration (an exact argument-free repeat of the name; annotations pile up until the nearest declaration, and a comment between them does not separate them) – the compiler rejects such a module docs | |
code/module-var-not-const |
✓ | file | A var / val / use declaration at MODULE level - only a constant lives there, an expression outside a method body is refused by the compiler and the apply rolls the project back docs |
|
code/param-redeclared |
✓ | file | A val / var / use inside a method body with the name of the method’s own parameter, nested blocks (a loop, a branch, try) included – a method is one scope with its parameters in it, the compiler answers “a variable named X is already defined” at the apply and the project rolls back; loop and catch variables, lambda parameters and full-form lambda bodies are not judged docs |
|
code/loop-header |
✓ | file | Malformed ‘for’ loop header docs | |
code/invalid-string-escape |
✓ | file | Invalid escape sequence in a string literal (\', regex-style \d) - the compiler rejects such a literal; valid are \н \в \т \\ \" \% \$ \ю<code> and the Latin spellings docs |
|
code/unused-local |
✓ | file | Unused local variable | |
code/unused-loop-var |
✓ | file | Unused loop variable | |
code/ref-field-needs-req |
✓ | file | Structure reference field without ‘req’ docs | |
style/boolean-compare |
✓ | file | Comparing a boolean value with True/False docs | |
style/undefined-is |
✓ | file | Checking Undefined with the ‘is’ operator docs | |
style/negated-is |
✓ | file | Negating the ‘is’ operator on the outside docs | |
style/semicolon-line |
✓ | file | ‘;’ not on its own line docs | |
style/wrap-operator |
✓ | file | Operator at the end of a wrapped line docs | |
style/wrap-comma |
✓ | file | Comma at the start of a wrapped line docs | |
style/camel-case |
✓ | file | Name is not in UpperCamelCase docs | |
style/const-case |
✓ | file | Constant is not in ALL_CAPS docs | |
style/exception-prefix |
✓ | file | Exception name without the exception marker - a prefix on a Russian name, the Exception suffix on a Latin one docs |
|
style/abbreviation-case |
✓ | file | All-caps abbreviation in a name docs | |
style/enum-name-vid |
✓ | file | Enumeration name starts with “Type” docs | |
style/collection-literal |
✓ | file | Manual collection fill instead of a literal docs | |
style/redundant-tostring |
✓ | file | An explicit ToString() call in a concatenation docs |
|
style/interpolation |
✓ | file | Concatenation instead of interpolation docs | |
style/type-colon-space |
✓ | file | Spaces around the type colon docs | |
style/union-spaces |
✓ | file | Spaces around ‘|’ in a union type docs | |
style/nullable-shorthand |
✓ | file | Undefined in a type without the ‘?’ shorthand docs | |
style/redundant-type |
✓ | file | Redundant type annotation on initialization docs | |
style/optional-params-last |
✓ | file | Optional parameter before a required one docs | |
code/resource-bare-name |
✓ | file | Resource{Resources/<file>.svg} - the key is a path RELATIVE to the Resources folder; spelling that folder out breaks the lookup docs |
|
query/named-parameter |
✓ | file | A named parameter &Name inside a query literal - the literal takes its values by interpolation (%Name) docs |
|
code/this-in-static-method |
✓ | file | The keyword this inside the body of a static method - a static method is common to the whole type and has no object context, the compiler rejects the project docs |
|
code/instance-call-from-static |
✓ | file | A bare call of an instance method of the same owner from a static method - the docs forbid it outright; call the method on a value or make it static docs | |
code/close-in-before-close |
✓ | file | Close() inside BeforeClose – the platform ignores the call and nothing closes the form afterwards |
|
query/no-isnull |
✓ | file | ISNULL( inside a query literal – the query language has no such function |
|
style/abstract-name |
✓ | file | An abstract variable name (Data, Item, Object, String, Value, Document in either spelling - exact or with a digit tail like Data1) says nothing about the variable; a stem inside a longer name (ClientData) and structure fields (a serialization contract) are left alone docs |
|
style/single-letter-name |
✓ | file | A single-letter name of a variable, parameter or loop variable - per the names standard one-letter names belong only to short lambda parameters ((A, B) -> A + B) docs |
|
style/negated-boolean-name |
✓ | file | A boolean variable named from the negation (NotConnected, NoErrors) - the name comes from the affirmative (Connected, HasErrors); judged only where the boolean type is proven: a type annotation or a boolean literal initializer docs |
|
style/type-in-name |
✓ | file | A variable name starting with a container type name (the Russian spellings of array, structure and map) - the type is visible from the declaration and the editor, keep it out of the name docs | |
style/numeral-in-const-name |
✓ | file | A spelled-out numeral in a constant name (TIMEOUT_ONE_MINUTE) describes the value - name the constant abstractly (TIMEOUT) so a value change does not break the name docs |
Tier D - semantics over stdlib, forms and the metamodel
Needs the project index and platform data: unknown types and objects, enumeration values, the execution model (client/server), form handlers, properties and queries.
| Rule | Scope | What it checks | ||
|---|---|---|---|---|
yaml/choice-needs-static-list |
✓ | file | ValueChoice without a static ChoiceList docs |
|
yaml/slot-needs-list |
✓ | file | A slot the ui schema types as Array<...> holding a single component instead of a list: the apply refuses such markup, and the lint used to keep silent docs |
|
yaml/value-choice-title |
✓ | file | A ValueChoice with an explicit SwitcherDisplayKind: Switcher sets a Title – the platform does not draw it and the field stays unlabeled; put the caption into a separate Label next to the switcher (nodes without an explicit kind and Array<...> ones – a checkbox group – are not judged) |
|
code/unknown-type |
✓ | project | Unknown type | |
code/catch-non-exception |
✓ | file | The type in catch is not an exception (a stdlib non-exception or a local structure) - the compiler rejects such code docs |
|
code/unknown-member |
✓ | file | A member access on a variable of a known stdlib type - plain or a generic, whose arguments type the members and do not name them - that the type does not have (first hop, typos get a hint) | |
code/member-kind-mismatch |
✓ | project | A stdlib method read as a property (or the other way round) docs | |
code/unknown-static-member |
✓ | project | A member reached through a type name (DateTime.Minimal()) that the type does not have; the type of such a call carries on to the next hop. A bare name is read as a type only when the project gives it no other meaning; the module’s paired yaml counts even in a single-file check |
|
yaml/foreign-not-public |
✓ | project | A yaml reference (a type position, a FormType navigation target, the root of a binding chain =Module.Method() or a qualified Subsystem::Element name) to an element of another subsystem whose VisibilityScope is not InProject/Global - unreachable from outside its subsystem, and no import helps; the qualified form resolves by the subsystem it names docs |
|
code/foreign-not-public |
✓ | project | A module names an element of another subsystem whose VisibilityScope is not InProject/Global – in a written type position or as the root of a Module.Method() chain, the qualified Subsystem::Element form included: the compiler rejects the reference at that line, no import helps, and a @InProject annotation on the method does not either. The project module belongs to no subsystem, so every non-public element is foreign to it docs |
|
code/call-arity-cross |
✓ | project | Argument count of a <Module>.<Method>(...) call outside the target module’s signature range docs |
|
code/undefined-name |
✓ | project | Undefined name in an expression (a typo in a name) and in a short string interpolation ("?$format=json" substitutes the name format, \$ is needed) - the compiler rejects such code |
|
code/unknown-object-type |
✓ | project | Unknown project-object type | |
yaml/unknown-type |
✓ | project | Unknown type in yaml | |
yaml/dynlist-missing-field |
✓ | project | Missing dynamic-list field docs | |
yaml/dynlist-row-editing |
✓ | project | An OnRowEdit handler on a list over a FLAT dynamic source: the event is declared for the node rows of a hierarchy, and on a flat list the platform never calls it - a click opens the object’s automatic form instead; give the object its own object form docs |
|
yaml/dynlist-joined-table-param |
✓ | file | A parameter (&Name) or a binding (=...) in the arguments or the filter of a JOINED table of a dynamic list: legal on the main table, never evaluated on a joined one – the compiler stays silent and the list fails at runtime; keep a literal in the yaml and assign the live value from code (Source.JoinedTables[i].Arguments) docs |
|
yaml/dynlist-filter-disabled |
✓ | project | A dynamic-list filter item is declared with Use: False while the paired module enables it by assignment: the first-render race – the platform draws the list without waiting for the code, and the first frame shows the whole table; declare the filter enabled with an empty value docs |
|
yaml/list-form-needs-dynlist |
✓ | file | The form inherits ListForm while its content holds a table over an ArrayDataSource and not a single type with DynamicList: the list-form skeleton is built around a dynamic-list table, and the navigation item silently disappears – give the table a dynamic list or inherit a plain form (Type: Form) docs |
|
yaml/ref-input-auto-commands |
– | file | A reference Edit with no Commands of its own: the platform draws its own button that opens the value in a separate window (for a reference input Auto unfolds into a command-interface fragment). The button is usually wanted, so the rule is informational and off; an empty fragment silences it docs |
|
yaml/toggle-command-pair |
✓ | file | Two adjacent UsualCommand nodes with mirrored Visible (=X against =not X) emulate one command with two states - the platform has the real thing: a SwitchableCommand carries the representations and images of both states, the initial Active is a literal, and the platform owns the state. A shared handler strengthens the case but is not required docs |
|
yaml/dynlist-column-sort-lost |
– | file | A column of a table over a dynamic list whose value CALLS something: the header will not sort, because the platform sorts by the FIELD of the source rather than by the text on screen. Bind the column to the field, or add a presentation field to the list itself. A column with DisableSorting: True is not judged – it has no sorting by declaration. Off by default: whether that column was meant to sort is not visible from the file docs |
|
yaml/badge-column-image |
✓ | file | A StandardTableColumn with Kind: Badge also sets Image – the platform does not show the picture: the value is drawn as tag pills and the picture is documented only for Kind: Picture; drop Kind (the picture stands next to the value text) or set Kind: Picture docs |
|
code/unknown-enum-value |
✓ | project | Unknown enumeration value docs | |
yaml/enum-needs-nullable |
✓ | project | Enumeration without nullable; judged in both spellings - the input field is recognized as Edit<...>, the platform’s own English (InputField is no spelling of it and falls to yaml/unknown-type) docs |
|
yaml/enum-default-value |
✓ | project | The DefaultValue of an enumeration-typed field must be the bare name of a declared value: the type-prefixed spelling (LabelVisibility.Invisible) or an unknown name is rejected by the build docs |
|
yaml/unknown-enum-value |
✓ | file | A component property value outside the enumeration of the ui schema (ContentVerticalAlign: End - the vertical axis has Top, Center, Bottom, Baseline and no End) |
|
yaml/bare-object-value |
✓ | file | A bare word on a property that accepts Object - the platform expects a quoted literal, an = binding or a $ localized-string reference docs |
|
code/unknown-resource |
✓ | project | The name in Resource{...} is neither in the project’s Resources folders nor in the platform’s image library docs |
|
form/unknown-handler |
✓ | project | Form handler not found in the module docs | |
form/handler-signature |
✓ | project | Handler signature does not match the event docs | |
code/unknown-form-component |
✓ | file | Access to a component the form markup does not declare docs | |
code/server-call-from-handler |
✓ | project | Server method is unavailable to a client handler docs | |
code/image-binding-server-call |
✓ | project | The Image property of a platform component is bound to an expression whose call – directly or transitively through client methods – resolves into a server method (an element module of a server kind, a common module with Environment: Server, a @OnServer method): the image arrives by its own server round-trip after the rows are drawn and is requested again on every redraw; hand it over with the data (a field of the query or of a joined table) or build it from client-side data docs |
|
code/client-annotation-in-server-module |
✓ | project | Client annotation in a server common module docs | |
code/client-module-in-http-service |
✓ | project | Client common module in a server environment docs | |
code/server-annotation-in-client-module |
✓ | project | Server annotation in a client common module docs | |
code/query-needs-server |
✓ | project | A Query{...} block in a method of a client-side module (a form, or a common module whose Environment involves the client) that carries no @OnServer - the type does not exist on the client and the compiler rejects the build docs |
|
code/local-method-cross-component |
✓ | project | Cross-component call of a local method docs | |
code/local-method-cross-module |
✓ | project | Cross-module call of a local method docs | |
naming/yo |
✓ | file | The letter yo in a name docs | |
naming/underscore |
✓ | file | Underscore in a name docs | |
naming/abbreviation |
✓ | file | All-caps abbreviation in a name docs | |
naming/latin-term |
✓ | file | English term spelled in Cyrillic docs | |
naming/enum-vid |
✓ | file | Enumeration name with the word “Type” docs | |
naming/kind-in-name |
✓ | file | Element kind inside its name docs | |
naming/filler-word |
✓ | file | Filler word in a name docs | |
naming/module-suffix |
✓ | file | Environment suffix in a common module name docs | |
naming/number |
✓ | file | Wrong number for the element kind docs | |
naming/boolean-name |
✓ | file | Boolean attribute name docs | |
naming/presentation |
✓ | file | Element presentation docs | |
naming/prefix-by-kind |
✓ | file | Kind-specific name without its prefix docs | |
code/unknown-ns-object |
✓ | project | Unknown object in a kind namespace | |
query/unknown-table |
✓ | project | Unknown table in a query docs | |
query/in-subquery-composite |
✓ | project | ‘IN’ with a subquery over a composite type docs | |
yaml/unknown-property |
✓ | file | Unknown object property | |
code/reserved-name |
✓ | file | Reserved name: the type keyword in either language (type, Type and the Russian spelling) as a structure field or a parameter - the server apply refuses all three (the capitalized one confirmed by a live apply) |
|
yaml/builtin-property-name |
✓ | file | Built-in property name clash | |
yaml/size-needs-no-stretch |
– | file | A size without disabling the stretch docs | |
yaml/col-width-needs-no-stretch |
– | file | A numeric Width on a table column (all three column kinds) without HorizontalStretch: when the column stretches the number acts as a share of the free space rather than pixels – the column comes out wider than asked and the content drifts away from its neighbour. A pixel width needs HorizontalStretch: False, a share with a guaranteed minimum – MinWidth. Off by default: width-as-a-share is a legitimate technique, statically indistinguishable from the trap docs |
|
yaml/matrix-group-max-width |
– | file | A numeric MaxWidth on a group that lays out as a matrix: the maximum is also the AVAILABLE width, so the automatic columns are laid out by it rather than by the window and a phone draws the page at desktop width (the content runs off the right edge). Answer Auto instead. Off by default: a desktop-only page lives with a maximum fine docs |
|
yaml/card-literal-stretch-weight |
– | file | A literal StretchWeight on a card or on a group inside one: the weight is a flex with a ZERO basis, and in a vertical column (the mobile layout) that basis applies to the HEIGHT - Safari collapses the card and clips it with the rounding, Chrome shows nothing. Drop the weight on a phone through a binding. Off by default: a card living only in a wide row keeps it legitimately docs |
|
code/unused-method |
– | project | Method is never referenced | |
code/duplicate-method-body |
– | project | A method body repeated word for word in ANOTHER file: the normalized body (comments, blank lines and indentation dropped) of at least five lines is compared. A platform hook is told apart by its @Handler annotation rather than by a list of names - the same hook body in every object is normal; copies inside one file are not judged. Off by default: whether two copies should become one method is a design decision |
|
yaml/missing-import |
✓ | project | A yaml reference (a type position, a FormType navigation target or the root of a binding chain =ForeignModule.Method()) to a public element of another subsystem that the Import section does not list – an import in the paired module does not cover the markup; a binding root is judged after subtracting everything that explains the name on its own: the declarations of this yaml, of the paired module and the implicit platform names docs |
|
code/unused-import |
✓ | project | A module imports a subsystem whose elements its CODE never mentions - the platform editor reports such imports, and they accumulate as the code that needed them is rewritten. A reference from the PAIRED yaml is not a use: the yaml has an import section of its own docs | |
code/missing-import |
✓ | project | A module names the type of a public element of another subsystem without an import line for it - the project fails to compile at that line. Both WRITTEN type positions (a parameter, a variable, a return, new, as, is, generic arguments) and the root of a chain (Module.Method()) are judged; for a root everything that explains the name on its own is subtracted first: the declarations of the method and the module, the implicit names of the platform and the sections of the PAIRED yaml docs |
|
yaml/missing-subsystem-usage |
✓ | project | Elements and modules of a subsystem import another subsystem while the description of their own (Подсистема.yaml) does not list it under Using - the project fails to apply, and that is learnt at deploy time. An import gives the short names, but it is Using that permits the subsystem; the diagnostic sits on the subsystem description, where the fix goes docs |
|
yaml/computed-binding-assigned |
✓ | project | Every instance of a component binds a property with a COMPUTED expression while the component assigns that property in its own module - the platform crashes on the assignment (IllegalStateException). A named argument is not an assignment, and a code-built instance, a bare-path binding, a literal or an unbound instance make the assignment legal - the rule fires only when every instance is bound computed | |
yaml/localization-missing-import |
✓ | project | An unqualified $Dictionary.Key whose dictionary lives in a subsystem this yaml does not import - the apply refuses the node as a not-imported namespace; an import in the paired module does not cover the markup, and the qualified $Subsystem::Dictionary.Key form needs no import docs |
|
yaml/presentation-field |
✓ | file | The presentation field of an object docs | |
yaml/unexpected-type-argument |
✓ | file | A type argument on a property the ui schema declares without one - another type, rejected when the build is applied (a form’s AdditionalCommands takes CommandInterfaceFragment, not CommandInterfaceFragment<UsualCommand>); an English tree is judged the same - the key, the component, the property and the type head are canonized, and the argument is compared with the default name by name in either spelling docs |
|
yaml/property-since-compat |
✓ | project | A component property newer than the project’s CompatibilityMode (the ui schema records the version it appeared in) - apply rejects it as an unknown property docs |
|
query/deletion-mark-immediate |
✓ | project | A deletion-mark condition in a query on an object whose DeletionMode is Immediately - such an object has no mark and the query fails on apply docs |
|
code/load-object-unwrap |
✓ | file | A force-unwrapped LoadObject() result on a reference from a field of another record or of a tabular-section row (Row.Service!.LoadObject()!) – the record may be deleted physically (DeletionMode: Immediately, the deleted-items form), and the unwrap fails the whole pass; check the result for Undefined (the query row’s own .Reference is not judged) docs |
|
yaml/item-id-required |
✓ | file | A metadata collection item (an attribute, a tabular section, an enumeration item, an access-key parameter) without the Id its class declares - apply answers ID required |
|
code/unknown-row-field |
✓ | project | A field addressed on a dynamic list row (DynamicListRow<Form.Type>) that the list’s Fields do not declare docs |
|
code/row-field-null |
✓ | project | A dynamic list field taken through a reference (Owner.Number) is ` |
|
yaml/unknown-attribute-property |
✓ | file | A key an attribute’s own metamodel class does not declare (Length on a regular attribute - the built-in Code declares it, a Number attribute has IntegerPartLength) - apply rejects the object |
|
yaml/empty-group-sized |
✓ | file | An empty Group with Height/Width (a literal – always; an =... binding – only without a Name) – the renderer drops the node and there is no gap |
|
yaml/insert-row-needs-align |
✓ | file | A horizontal group holding an HtmlContainer insert and no VerticalContentAlignment: children are laid out on the BASELINE, and the insert carries one of its own, so the element holding it slides down against its neighbours (50 px on a live row). The nearest horizontal ancestor answers, so a row whose inner strip is already aligned stays silent docs |
|
yaml/hint-too-long |
✓ | file | A Tooltip longer than the render limit – the tail is not shown at all |
|
yaml/popup-in-markup |
✓ | project | A PopupComponent (or a project component transitively inheriting it) placed in the yaml markup: the content is drawn right in the form flow before the window ever opens – the platform has no property restricting the drawing to the window, and hiding it via Visible breaks the window itself; build the window in code on every opening – a new PopupComponent(...) followed by OpenInPopupWindow() docs |
|
yaml/date-input-needs-plain-date |
✓ | file | Edit<Date?> – the renderer silently drops a date input that allows the empty value; make the type plain and express “not set” with the empty date docs |
|
yaml/binding-needs-auto |
✓ | project | A binding of a plain component property calls a method declared nullable - the client registers an “unexpected Undefined value” error on every recomputation; “not set” is the Auto value | |
code/client-available-needs-context |
✓ | project | @AvailableFromClient on a method of an interface component module that is neither static nor @Contextual – the component type is not a singleton, so the apply rejects the modifier docs |
|
code/client-available-unused |
– | project | A method declared @AvailableFromClient with no client place in the project naming it - neither a module of the client environment, nor a client method of a server module, nor a yaml, nor a string literal. The annotation opens a surface to the client that nobody uses. Off by default, like code/unused-method: a client call is not always visible statically docs |
|
code/server-module-in-client-context |
✓ | project | A Module.Member(...) access to a common module with Environment: Server from a method that runs on the client (an interface component, a command, a client common module) – the type does not exist on the client docs |
|
code/component-in-server-context |
✓ | project | A Component.Member(...) access to an interface component from code compiled for the server – a @OnServer method anywhere, or an unannotated method of a server or client-and-server module: the component’s type lives on the client, and the server compilation refuses with “Variable X is not defined” docs |
|
yaml/delete-current-needs-immediate |
✓ | file | OnReferencedObjectDeletion: DeleteCurrent on an attribute whose owner has a DeletionMode that only marks (DeletionMark is also the default) – the apply answers Action DeleteCurrent cannot apply to object with a DeletionMark docs |
|
code/access-context-read-noop |
✓ | project | Extending the access context with the read privilege for a type whose yaml says Read: PermitEveryone: everyone may read it already, so there is nothing to grant - the call only suggests the data is guarded. With that privilege alone the whole line goes; among others, only it does docs |
|
code/per-object-permissions-need-common |
✓ | project | An object calculates its permissions per object, but its module declares no ComputeAccessPermissions handler – the common calculation is required even then, if only to return an empty array docs |
|
code/permission-field-not-declared |
✓ | project | Inside ComputeAccessPermissionsForObjects a field outside ComputePermissionsBy is read, or a declared field is reached through Entity instead of the record docs |
|
code/permission-handlers-need-recalc |
✓ | project | A module declares a permission handler (ComputeAccessPermissions and kin) while the project calls RecomputeAccessPermissions for that entity nowhere - the platform never calls the handler by itself, so a permission edit silently does not act; a recompute with a non-entity receiver (the documented loop form) stands the rule down, kinds with no recompute method (rights elements) are not judged docs |
|
code/permission-right-not-computable |
✓ | project | Handler ComputeAccessPermissions (or ...ForObjects) grants a permission the entity’s yaml does not declare computable (PermissionsComputed / PermissionsComputedForEachObject, explicitly or through Default) – the build applies, and the permission recomputation fails at runtime: the permission is not marked as computed. Permissions are collected only from new AccessPermission(...) constructors in both namespaces (Entity.Privilege.*, HttpServicePrivilege.*), transitively over project calls – delegation into a shared rights module is followed and the finding is bound to the entity; AccessContext.Append does not count, under-granting is legal, kinds without access control are not judged docs |
|
yaml/placeholder-key-in-strings |
✓ | file | A key carrying the placeholder $0 in the Strings section of a LocalizedStrings dictionary: the section compiles to a method WITHOUT parameters, so a call with an argument fails the apply with an “unknown method” answer docs |
|
yaml/localization-ref-to-template |
✓ | project | A $Dictionary.Key reference pointing at a key of the Templates section: a reference resolves against Strings alone, and the apply fails with “localized string not found” (the stand rolls back). A template key nobody references is left alone - code calls it legitimately docs |
|
code/compare-with-localized |
✓ | project | A localized value (Dictionary.Key(), Presentation()) compared against a literal or against a second localized value – in another language the branch simply never runs docs |
|
code/url-params-partial-encoding |
– | file | A call of the Url method WithRequestParameters: it encodes a parameter value only partially – “&” and “=” inside the value stay separators, and a value that is itself an address arrives cut at its first “&”; build the string with the parameters object and glue it to the base address. Off by default: whether a value can carry “&” is not statically visible docs |
|
code/bound-property-assign |
✓ | file | A property COMPUTED by an expression in the paired markup (Height: =Common.IsNarrowScreen()?820:528) is assigned from code - the platform refuses such an assignment, and inside a try/catch the refusal is invisible; a data binding (a bare path) is left alone, it is two-way by design |
|
yaml/event-needs-importance |
✓ | file | An EventLogEvent description that does not set Importance: its default is FromConstructor, so the platform then demands the value in EVERY constructor, and one write that omits it fails the apply on the constructor line; an explicit Importance: FromConstructor states the choice and silences the rule docs |
|
yaml/event-property-type |
✓ | file | An EventLogEvent property type outside the platform’s closed list: a project enumeration cannot go there – the refusal comes only from the server-side compilation and costs the deploy; the list is read from the metamodel (EventLogEventProperty.Type), ? and a Std:: qualification are tolerated, variant values are written as string codes with the allowed codes listed in the property’s Description docs |
|
code/collection-field-needs-req |
✓ | file | A structure field whose generic type has no argument-less constructor (ReadableArray<String>) and no req, ? or initializer - the apply answers “cannot be initialized with a default value”; Array<String> and the like are constructible empty and are left alone docs |
|
code/var-needs-init |
✓ | project | A variable declared by type alone where the type has no constructor and no default value (var Response: HttpResponse) - the compilation answers “has neither a constructor nor a default value”; an enumeration, an annotation, a singleton and a name shadowed by a project type are skipped docs |
|
code/unknown-tabular-member |
✓ | project | A member access on a tabular section’s row collection that the array type does not have (Object.Section.Member in an object form module, the bare section name or this.Section in the entity’s modules) - the collection is Array<Entity.Section>, and the other platform’s habitual Count() is called Size() here; a module named after the section shadows it, attributes are not judged |
|
code/global-unavailable |
✓ | project | A call of a global name outside its environment: Message (client-only) in a server module - the apply answers “the method is unavailable in the current environment”, the dynamic evaluation globals (server-only) in a client method without @OnServer; @OnClient/@OnServer override the module’s environment, the availability comes from the per-member availability lines of the global context packages docs |
|
style/shadow-project-name |
✓ | project | A variable, parameter or method named like a project element (a Warehouses variable next to the Warehouses catalog) - the declaration shadows the element for that scope; platform handler parameter names never collide with project names docs |
|
style/shadow-own-property |
✓ | project | A local VARIABLE named like a property of the element the module belongs to: inside the method the name resolves to the variable, so an assignment never reaches the property. Judged only where such a property is in scope - the module of an interface component and the object module; a parameter of that name is the ordinary way to pass a value in and is left alone docs | |
code/unclosed-resource |
✓ | file | A closeable resource (val Selection = Query{...}.Execute()) abandoned by an early exit from the loop over it: the platform closes a full pass by itself, while a return or a break in the middle leaves the resource open and the platform logs an unclosed-resource event; declaring the variable with use closes it on every exit path. A resource that arrived as a parameter, one the method closes by hand and one it returns to its caller are left to the author docs |
|
code/use-needs-closeable |
✓ | file | The use modifier over a type the catalog describes and that does not inherit Closeable - the modifier exists for the automatic Close(), and the compiler refuses the declaration docs |
|
conventions/untranslated-visible-literal |
✓ | project | Visible text left as a Cyrillic literal where the project already references the same property into a localization dictionary - the intent is counted per element kind, so a same-named property of another kind is not judged; silent on a project whose descriptor lists fewer than two localization languages | |
conventions/untranslated-code-literal |
– | project | Visible text left as a Cyrillic literal in a MODULE - judged by the SINK it reaches (an argument of the platform message call, a property of an event-log event, or either of them one step away through a method that forwards its parameter); markup, pure interpolation and single words are skipped, and the rule is silent on a project whose descriptor lists fewer than two localization languages | |
conventions/missing-translation |
– | project | A project token or a Cyrillic comment line the project’s translation dictionary does not cover yet - one finding at its first occurrence in the file; silent unless an xbsl-translation dictionary lives next to (or above) the project (see xbsl translate) |
|
code/unknown-structure-field |
✓ | project | A field access on a structure declared IN THE PROJECT is checked against its declaration: rename a field and its reader in another module turns red here rather than on the server apply. The type comes from the variable’s declaration (Module.Structure, a bare name for the declaring module), from a new constructor and from the element type of a for X in List loop; a name declared with anything else in the method, a namesake of a stdlib type, the second hop of a chain and Latin member spellings are not judged |
Group details
Queries: IN with a subquery over a composite type (rule query/in-subquery-composite)
A platform standard: IN with a subquery over an expression of a composite type is implemented
inefficiently on most DBMSs, so the condition is written with EXISTS instead. The rule is a
warning – the standard is mandatory:
WHERE T.Value IN (SELECT F.Value FROM Filters AS F) // warning
WHERE EXISTS (SELECT 1 FROM Filters AS F WHERE F.Value = T.Value) // this way
A type counts as composite when the yaml spells two or more alternatives (String|Number|?): the
? is not a type but the admissibility of Undefined, and Array<String|Number> is not
composite either. Only a field whose type is known for sure is questioned: Alias.Field or
Table.Field, where the alias is unambiguous within the block and the field is found in the
table’s yaml; a list of values (IN (1, 2, &Codes)) is not what the standard is about. Both
spellings of the query language are understood - the English IN, NOT, SELECT and their
Russian equivalents.
Project properties (the project/ rules)
Four rules from the standard “Filling in the project properties”: Vendor and Name are
identifiers built from the presentations, every word capitalized; Presentation and
VendorPresentation are filled in - the
official name of the project and of the company that developed it; Version is three numbers
A.B.C (semantic versioning), not 1.0.
Names of project elements (the naming/ rules)
Twelve rules from the platform standard “Names of project elements” – it is mandatory in new code,
so all of them are warnings. They read the descriptions (.yaml): the name of the element itself
and the names in its Attributes, Dimensions, Resources, TabularParts and enumeration
values.
The number of a name is checked against the kind: catalogs, documents, registers and tabular
sections are named in the plural, enumerations and structures in the singular (naming/number).
For a Russian name this is morphology, not a guess by the ending: a singular noun that the
standard allows is told apart from a plural that reads as a genitive singular without the case.
Needs the [morph] extra (pip install "xbsl[morph]"); without it Russian names stay silent.
An English name (a translated tree) is judged by its last word with suffix heuristics and the
irregular plurals listed, and needs no extra; mass nouns and ambiguous tails are left undecided.
The rest: the letter yo and underscores in names, an abbreviation written in mixed case instead
of all caps, an English term transliterated rather than kept as the original (Xml, not its
Cyrillic spelling), an enumeration named with the word for type where the standard asks for the
word for kind, the element kind repeated inside its own name, filler words such as the ones for
management or manager, an environment suffix on a common module name (the environment is a
property, not a name), a boolean attribute named by a negation instead of the positive form, an
empty Presentation, and the prefixes required for certain kinds - access key, right and
navigation.
Code style conventions (the style/ rules)
Twenty-eight rules that follow the platform documentation (“Code style conventions”, “Language
idioms”) and the “Variable and constant names” development standard: layout and expression
wrapping, naming, type descriptions and signatures, collection literals, string interpolation,
and checks of boolean values and Undefined.
Of the variable-names standard the token-provable part is checked: abstract names,
single-letter names outside lambdas, Cyrillic and Latin abbreviations not written as one word,
boolean names built from the negation, a container type inside a name, numerals inside constant
names, and the shadowing of project element names. Left to the author and review: redundant
words in a name, abbreviations beyond the capitalization law, digits in place of a qualifier
over a meaningful stem (Stage1 and Data1 differ only in meaning), and the abstractness of
a constant name beyond numerals (the role of INITIAL_STAGE against the value of
STAGE_QUESTIONNAIRE is invisible to tokens).
Rules that clean code already satisfies are enabled by default (warning) – they guard against
regressions. Rules that typically fire on accumulated legacy debt are info and disabled; enable
them to measure the debt and pay it down:
xbsl path/to/sources --select style # ONLY these rules (replaces the default set)
xbsl path/to/sources --enable style # the default set PLUS these
xbsl path/to/sources --ignore style # the default set minus these
--select, --enable and --ignore accept a rule id, a group (the part before /) or a tier
letter, repeated or comma-separated. --select narrows to exactly the given rules; --enable
switches on off-by-default rules on top of the defaults.
Query{ ... } blocks (the query DSL) and string literals (HTML/CSS/SVG in web views) are
excluded from these checks. Not covered, and left to the author and review: indentation being a
multiple of four, collection idioms, Rows.Join() for bulk concatenation, the ?. / ??
idioms, and case instead of an else if chain.
Code semantics (the code/ rules)
The largest group - sixty-four rules, thirty-four of them errors. This is what the compiler rejects
or what the platform does differently from how the code reads: an unknown name or member, the
arity of a call, the environment (client code in a server method and the other way round), an
instance reached through its type, a caught non-exception, an unclosed resource, a walk over a
collection while it is being changed, and the platform traps whose only sign is the shape of the
code. Some of the rules are project-scoped (--stdin does not run those): they need the paired
yaml and the names of the objects.
Element descriptions (the yaml/ rules)
Fifty-seven rules over the descriptions (.yaml): required and unique ids, known keys and types,
references to components, handlers and localized strings, what the platform requires of field
types (a reference and an enumeration admit an empty value), the settings of dynamic lists and
forms, and the layout traps that apply without an error yet draw differently from the intent.
Six rules are info and off: they say “this is how the platform works”, not “this is a mistake”.
Project conventions (the conventions/ rules)
Rules about what the PROJECT agreed on rather than what the platform demands. The base set
carries the bilingual-project family: conventions/untranslated-visible-literal (on by
default) reports visible text left as a Cyrillic literal where the project already routes the
same property through the localization dictionary, and conventions/untranslated-code-literal
with conventions/missing-translation (both off) extend that to module literals and to the
translation dictionary - whether every human-readable string must come from the dictionary is
a per-project decision, so the base set does not impose it.
The group is also the extension point by design: a project plugin registers its own house
rules under conventions/ (a ban on task numbers in comments, internal references and the
like) and decides their severity and defaults for that project - see
Extending. Runtime truth is
xbsl --list-rules; the table above lists the base set only.
The small groups
typography/- typographic characters in prose and comments: em dash, the ellipsis character, curly quotes, guillemets in comments, plus the letter “ё” in the text a user reads;whitespace/- trailing spaces and mixed newlines;encoding/- a file that is not UTF-8;structure/- the pairing ofName.yamlandName.xbsl;security/- a secret in the sources (a token, a password, a key);form/- a form handler the module does not have, and a handler whose signature contradicts the event of the component (project-scoped rules);query/- queries: an unknown table,ISNULL, a named parameter, an immediate deletion mark and the standard aboutINwith a subquery (discussed above).
Enabling and disabling
--select and --ignore accept a rule identifier, a group (the part before /, e.g. style)
or a tier letter A/B/C/D. A plugin may override a rule’s severity (the xbsl.severity
entry-points group); XBSL_NO_PLUGINS=1 disables plugins and restores the built-in values from
this table.