Formatting Rules
Oidis enforces consistent formatting through the Oidis Builder's lint configuration. These rules are checked by pre-commit hooks and CI pipelines. The configs live in io-oidis-builder/resource/configs/.
TypeScript Formatting
Whitespace & Layout
| Rule | Value |
|---|---|
| Indentation | 4 spaces (no tabs) |
| Max line length | 140 characters (template literals exempt) |
| Trailing whitespace | Not allowed |
| Consecutive empty lines | Max 1 |
| Empty lines at file start | 0 |
| Empty lines at file end | Max 1 |
case inside switch | Not indented from switch |
| Multiline extends/arrays | Align initializers |
// ✅ Correct — switch/case alignment
switch (status) {
case RecordStatus.ACTIVE:
this.processActive();
break;
case RecordStatus.INACTIVE:
this.processInactive();
break;
default:
LogIt.Warning("Unknown status", status);
}
Semicolons & Quotes
| Rule | Value |
|---|---|
| Semicolons | Always required |
| Quotes | Double quotes only (") |
| Template literals | Allowed (backticks) |
| Trailing commas | Never |
| Floating decimals | Not allowed (0.5 not .5) |
| Object property quotes | Consistent-as-needed |
// ✅ Correct
const name = "John";
const message = `Hello ${name}`;
const rate = 0.5;
const config = { host: "localhost", port: 3000 };
// ❌ Incorrect
const name = 'John'; // Single quotes
const rate = .5; // Floating decimal
const items = [1, 2, 3,]; // Trailing comma
Spacing Rules
Type colon — space before AND after:
// ✅ Correct
public Name : string;
private count : number = 0;
public async FetchData() : Promise<void> { }
const result : boolean = true;
// ❌ Incorrect
public Name: string;
private count: number = 0;
Imports — spaces within braces:
// ✅ Correct
import { LogIt } from "@io-oidis-commons/.../Utils/LogIt.js";
import { BasePanel, BasePanelController } from "@io-oidis-gui/.../BasePanel.js";
// ❌ Incorrect
import {LogIt} from "@io-oidis-commons/.../Utils/LogIt.js";
Arrow functions — always parentheses, always space:
// ✅ Correct
const handler = () => { };
const transform = ($item) => $item.Name;
items.forEach(($item) => { process($item); });
// ❌ Incorrect
const transform = $item => $item.Name; // Missing parens
const handler = ()=> { }; // Missing space
Function parentheses:
| Context | Rule |
|---|---|
| Anonymous function | Always: function () { } |
| Named function | Never: function myFunc() { } |
| Async arrow | Always: async () => { } |
| Catch | Always: catch (ex) { } |
- No
$prefix —exis an exception binding, not a method parameter. Writecatch (ex), nevercatch ($ex). - No
: anyannotation —useUnknownInCatchVariablesis off, soexis implicitlyany. Writingcatch (ex : any)is redundant noise. - If strict catch typing is ever enabled, the correct pattern would be
catch (ex : unknown)with proper narrowing — but that is not the current state.
Infix operators — always spaced:
// ✅ Correct
const total = price + tax;
const isValid = count > 0 && name !== "";
const result = condition ? valueA : valueB;
// ❌ Incorrect
const total = price+tax;
Operator linebreak — after, not before:
// ✅ Correct — operator at end of line
const result = longConditionA &&
longConditionB &&
longConditionC;
// ❌ Incorrect — operator at start of next line
const result = longConditionA
&& longConditionB
&& longConditionC;
Comments
// ✅ Spaced comments (always a space after //)
// This is a comment
// TODO: Fix this later
// ✅ Special markers allowed
//! Important note
// TODO: Refactor this
// ❌ No space after //
//This is wrong
TypeScript-Specific Rules
Array types:
// ✅ Simple types — use bracket syntax
const items : string[] = [];
const counts : number[] = [];
// ✅ Complex types — use Array<> generic
const maps : Array<Map<string, number>> = [];
const handlers : Array<($item : Item) => void> = [];
Type assertions:
// ✅ Correct — angle bracket syntax
const element = <HTMLDivElement>document.getElementById("container");
// Note: This is the Oidis convention (not the `as` syntax)
Import extensions:
// ✅ Always .js extension
import { MyClass } from "./MyClass.js";
import { LogIt } from "@io-oidis-commons/.../Utils/LogIt.js";
Return await — always use:
// ✅ Correct
async function getData() : Promise<Data> {
return await fetchData();
}
// ❌ Incorrect
async function getData() : Promise<Data> {
return fetchData(); // Missing await
}
Method signatures — always inline:
Method signatures must be written on a single line, including all parameters and the return type. Do not split parameters across multiple lines. If the line exceeds the 140-character limit, consider shortening parameter names or extracting a parameters interface — but never break the signature into multiple lines.
// ✅ Correct — inline signature
public GroupIntervals($report : IEDCParsedReport, $groupType : "15m" | "1h" | "1d" | "1m") : IGroupedInterval[] {
// ✅ Correct — private method, 4 parameters, still inline
private parseRow($columns : string[], $header : IEDCReportHeader, $lineNumber : number, $warnings : string[]) : IEDCInterval {
// ❌ Incorrect — multiline signature
public GroupIntervals(
$report : IEDCParsedReport,
$groupType : "15m" | "1h" | "1d" | "1m"
) : IGroupedInterval[] {
Statement compactness — prefer inline:
Single-statement expressions (function calls, assignments, returns) should stay on one line when they fit within the 140-character limit. Do not break short expressions across multiple lines for aesthetic reasons — this reduces readability by scattering simple logic across unnecessary vertical space. Multiline is acceptable only when the line genuinely exceeds 140 characters or the expression has complex nested structure (e.g. object literal with multiple properties, chained conditions).
// ✅ Correct — fits on one line, keep it compact
LogIt.Info(`Batch ${batchIndex + 1}/${batches.length}: executing ${batch.length} steps in parallel...`);
// ✅ Correct — simple assignment, one line
const stepNums : string = batch.map(($s : IExecutionStep) => String($s.stepNumber)).join(", ");
// ❌ Incorrect — unnecessary multiline for a single function call that fits on one line
LogIt.Info(
`Batch ${batchIndex + 1}/${batches.length}: executing ${batch.length} steps in parallel...`
);
// ✅ Correct — genuinely long line (exceeds 140 chars) → multiline is acceptable
const result : IExecutionResult = await this.processEngine.ExecuteWithRetry(
pipeline, context, { maxRetries: 3, backoffMs: 1000 }
);
This is not about cramming code into fewer lines — it is about avoiding artificial line breaks that hurt scan-ability. When a statement fits within the line limit and reads naturally on one line, keep it there.
Explicit types everywhere — avoid type inference:
Oidis aims for maximum type explicitness. Even when TypeScript can infer types, always provide them explicitly. This includes arrow function parameters and return types in .map(), .filter(), .find(), .sort(), .reduce() and other callbacks. The linter does not yet enforce this strictly, but it is the target convention.
// ✅ Correct — explicit parameter and return types
const producers : IEDCReportEAN[] = eans.filter(($ean : IEDCReportEAN) : boolean => $ean.type === "D");
const names : string[] = items.map(($item : IRecord) : string => $item.Name);
result.sort(($a : IGroupedInterval, $b : IGroupedInterval) : number => $a.periodStart.getTime() - $b.periodStart.getTime());
// ❌ Incorrect — relying on type inference
const producers = eans.filter(($ean) => $ean.type === "D");
const names = items.map(($item) => $item.Name);
result.sort(($a, $b) => $a.periodStart.getTime() - $b.periodStart.getTime());
This makes the code self-documenting — any developer can immediately see what types flow through the chain without hovering in an IDE. It also catches type errors at the point of definition rather than at the point of use.
Restricted Patterns
| Pattern | Severity | Alternative |
|---|---|---|
console.* | Error | Use LogIt |
querySelector | Warning | Use data-oidis-bind |
addEventListener | Warning | Use EventsBinding / GuiCommons.getEvents() |
| Anonymous object types | Warning | Define an interface |
Non-null assertion (!) | Error | Use ObjectValidator.IsEmptyOrNull() |
=== null / !== null | Convention | Use ObjectValidator.IsEmptyOrNull() |
isNaN() | Convention | Use ObjectValidator.IsDigit() |
Direct null comparisons (=== null, !== null) and native isNaN() are convention violations. Always use ObjectValidator methods instead. The linter does not currently enforce this, so it requires manual discipline.
// ✅ Correct — null/empty checks
if (ObjectValidator.IsEmptyOrNull(date)) {
if (!ObjectValidator.IsEmptyOrNull(interval)) {
// ❌ Incorrect
if (date === null) {
if (interval !== null) {
// ✅ Correct — numeric validation
if (!ObjectValidator.IsDigit(value)) {
if (ObjectValidator.IsDigit(inValue)) {
// ❌ Incorrect
if (isNaN(value)) {
if (!isNaN(inValue)) {
Readability over Cleverness
Oidis prioritizes readable, straightforward code over terse or clever constructs. The goal is that any junior or mid-level developer can understand the code without deep language expertise or additional documentation.
Guidelines:
- Prefer well-known, explicit patterns over obscure but shorter alternatives
- Do not optimize for keystroke count — optimize for comprehension
- If a construct requires a "how does this work?" explanation, use a simpler alternative
- Complex and efficient code is fine when truly needed (performance-critical paths), but it must be clearly commented
// ✅ Clear — anyone knows what .values() does
for (const intervals of groups.values()) {
// ...
}
// ❌ Clever — requires knowledge of destructuring skip syntax
for (const [, intervals] of groups) {
// ...
}
// ✅ Clear — explicit condition
if (items.length === 0) {
return;
}
// ❌ Clever — bitwise NOT trick for indexOf
if (~items.indexOf(target)) {
// ...
}
This does not mean we write simplistic code — Oidis uses advanced patterns (decorators, generics, reflection, async streams). The rule is: use the simplest construct that correctly expresses the intent. When a more complex approach is necessary, document why.
Error Handling & Return Values
Oidis does not prefer exception-driven control flow. Exceptions are reserved for truly critical errors (infrastructure failures, unrecoverable states) that should propagate to logging/Sentry. For expected failure cases (invalid input, missing data, parse errors), use return values — not exceptions.
Public API methods should return a result object that always has a consistent structure, including a state or error indicator. The caller should never need to check for null — instead, it inspects the result's state.
// ✅ Correct — public API returns a complete result with error state
public Parse($csvContent : string) : IEDCParsedReport {
if (ObjectValidator.IsEmptyOrNull($csvContent)) {
errors.push("CSV obsah je prázdný");
return {header: ..., intervals: [], warnings, errors}; // Always a valid object
}
// ...
}
// Caller never checks for null:
const report : IEDCParsedReport = parser.Parse(csvContent);
if (report.errors.length > 0) { ... }
Private/internal methods may return null to signal "skip this item" within a processing loop — this is acceptable because the scope is limited and the caller is within the same class.
Low-level parsing utilities (ParseCzechDecimal, ParseCzechDate) may return null or NaN as standard sentinel values — this follows the convention of native parsing functions (parseFloat, parseInt). However, prefer ObjectValidator for checking these values at the call site rather than direct === null or isNaN().
Rule of thumb: The more public a method is, the more it should return a proper result object with state. Private helpers and low-level utilities can use null/NaN as long as the top-level public method aggregates everything into a clean result.
Unused Variables
The linter ignores certain unused variables by convention:
| Pattern | Purpose |
|---|---|
$-prefixed args | Callback parameters you don't use |
ex / e in catch | Exception variable in empty catch blocks |
Variables starting with T / I | Generic type parameters |
Test files (*Test.ts) have relaxed rules: unused vars off, empty functions allowed, prefer-const off.
JSDoc Comments
JSDoc is required on public API methods — but it must follow the Documentation Philosophy: every JSDoc must add value, never restate what code already says. For full rules, approved tags, and format, see JSDoc Conventions.
Inline comments (//) are used only for non-obvious logic — complex algorithms, non-intuitive business rules, or intentional workarounds.
Completeness rule: If a JSDoc block is present (production or test code), it must be complete — the linter enforces @returns on any documented method that returns a value.
Test files: In test files (*Test.ts), JSDoc blocks are acceptable for describing test intent or test helper methods, even on private methods.
// ✅ Public API — JSDoc required (adds value: explains format placeholders)
/**
* @param {string} $message Log message with format placeholders.
* @param {any[]} $parameters Format parameters.
* @returns {void}
*/
public Info($message : string, ...$parameters : any[]) : void {
// ✅ Public API — JSDoc may be omitted (getName on Name property is self-evident)
public getName() : string {
// ✅ Test helper — JSDoc with complete @returns
/**
* Creates a multi-day CSV with known values for testing.
* @returns {string} Returns the CSV string with 8 intervals.
*/
private getTestCSV() : string {
// ❌ Incomplete JSDoc — missing @returns
/**
* Creates a multi-day CSV with known values for testing.
*/
private getTestCSV() : string {
File Structure — Class First
Each file should ideally contain one primary class whose name matches the file name. When the file also contains helper interfaces, enums, types, or secondary classes, these should be placed after the primary class, not before it.
This improves DX — when a developer opens a file, they immediately see the main class and its logic. Supporting types are visible below and their relationship to the class is clear from context.
// ✅ Correct — class first, interfaces after
import { ObjectValidator } from "@io-oidis-commons/.../ObjectValidator.js";
export class EDCReportParser {
public Parse($csvContent : string) : IEDCParsedReport {
// ...
}
private parseHeader($headerLine : string, $errors : string[]) : IEDCReportHeader {
// ...
}
}
export interface IEDCParsedReport {
header : IEDCReportHeader;
intervals : IEDCInterval[];
warnings : string[];
errors : string[];
}
export interface IEDCReportHeader {
eans : IEDCReportEAN[];
dateRange : IDateRange;
}
// ❌ Incorrect — interfaces before the class push the main logic down
import { ObjectValidator } from "@io-oidis-commons/.../ObjectValidator.js";
export interface IEDCParsedReport { ... }
export interface IEDCReportHeader { ... }
export interface IEDCInterval { ... }
export class EDCReportParser {
// The reader has to scroll past all interfaces to reach the class
}
This is a convention, not a linter rule. TypeScript allows forward references to interfaces and types, so placing them after the class does not cause compilation issues.
Class Member Ordering
The linter enforces strict member ordering within TypeScript classes (severity: warn). Members must appear in this order:
| # | Category | Notes |
|---|---|---|
| 1 | Call signatures | |
| 2 | Static fields | public → protected → private |
| 3 | Decorated fields | Fields with decorators (e.g., @Property) |
| 4 | Instance fields | public → protected → private |
| 5 | Static accessors | static get/set |
| 6 | Static methods | |
| 7 | Constructors | |
| 8 | Decorated methods | Methods with decorators (e.g., @Route) |
| 9 | Instance accessors | get/set |
| 10 | Instance methods | |
| 11 | Abstract members | abstract fields and methods |
// ✅ Correct ordering
export class ProductRecord extends BaseRecord {
// 2. Static fields
public static readonly TYPE : string = "Product";
// 3. Decorated fields
@Property({ type: String })
public Name : string;
// 4. Instance fields
private isLoaded : boolean = false;
// 7. Constructor
constructor() {
super();
}
// 8. Decorated methods
@Route("/product/:id")
public async GetProduct($id : string) : Promise<void> { ... }
// 10. Instance methods
public Validate() : boolean { ... }
private resetState() : void { ... }
}
JSONP / Config Files
JSONP files (.jsonp) are used throughout Oidis Builder for project configuration — package.conf.jsonp, commons.jsonp, import files like bootstrap.jsonp, resource templates, etc. Despite the .jsonp extension, they are JavaScript source files that use a specific function call wrapper, and their inner object structure follows standard JSON formatting conventions — not TypeScript conventions.
The TypeScript rule prop : Type (space before and after :) applies only to TypeScript type annotations. JSONP object properties use standard JSON style: property: value — no space before the colon, no column alignment.
Rules
| Rule | Value |
|---|---|
| Indentation | 4 spaces (no tabs) |
| Colon spacing | No space before :, one space after: property: value |
| Column alignment | Never — no padding to align values |
| Property name quotes | Omit when valid JS identifier; required for special chars ($interface, @type) |
| Trailing commas | Never |
| String quotes | Double quotes (") |
// ✅ Correct — standard JSON style, no alignment
Io.Oidis.Builder.DAO.Resources.Data({
$interface: "IProjectSolution",
e2eTest: {
email: "",
password: "",
groupName: ""
},
target: {
resources: {
e2eConf: {
input: "resource/configs/e2e.conf.template.jsonp",
output: "../../test/e2e/e2e.conf.jsonp"
}
}
}
});
// ❌ Incorrect — TypeScript-style alignment applied to JSONP (wrong)
Io.Oidis.Builder.DAO.Resources.Data({
$interface: "IProjectSolution",
e2eTest: {
email : "", // ← space before : (TS type-colon rule — does not apply here)
password : "",
groupName: "" // ← inconsistent result of manual alignment
}
});
Quoted vs. unquoted property names
// ✅ Correct — unquoted where possible
Io.Oidis.Builder.DAO.Resources.Data({
$interface: "IProjectSolution", // $ is valid in JS identifiers
domain: {
location: "irma.oidis.io"
}
});
// ✅ Correct — quoted when required (special chars, reserved words)
Io.Oidis.E2e.DAO.Resources.Data({
"$interface": "Io.Oidis.E2e.Config", // dot notation — must be quoted
"baseUrl": "http://localhost:9306"
});
SCSS Formatting
Base Configuration
Extends: stylelint-config-clean-order/error, stylelint-config-standard-scss, stylelint-config-sass-guidelines
Key Rules
| Rule | Value |
|---|---|
| Max nesting depth | 6 (ignores @each, @media, @supports, @include) |
!important | Not allowed |
| Vendor prefixes | Not allowed (use autoprefixer) |
| Hex colors | Long uppercase (#FFFFFF, not #fff) |
| Strings | Double quotes |
| Leading zero | Not allowed (.5 not 0.5) |
| Zero units | No units (0 not 0px) |
| Color functions | Legacy syntax with with-alpha |
| Alpha values | Number notation |
| Media queries | Prefix notation (range syntax) |
TypeScript requires 0.5 (no floating decimals), but SCSS requires .5 (no leading zero). Each follows its own ecosystem's convention.
// ✅ Correct SCSS
.MainPage {
display: flex;
background-color: #395A2D;
padding: 0;
opacity: .5;
font-family: "Plus Jakarta Sans", sans-serif;
.ListContent {
margin-bottom: 16px;
}
}
// ❌ Incorrect
.MainPage {
background-color: #395a2d; // Should be uppercase
padding: 0px; // No units on zero
opacity: 0.5; // No leading zero in SCSS
font-family: 'Plus Jakarta Sans'; // Single quotes
}
CSS Class Naming
- Oidis classes: PascalCase (
.MainPage,.ProfileImage) - Bootstrap classes: kebab-case (
.btn-primary,.d-flex) — follow Bootstrap 5 docs - SCSS variables/functions/mixins: camelCase or kebab-case
Verification
Run the linter to check formatting:
oidis.cmd project-cleanup test:lint
This runs both TypeScript and SCSS linting, plus copyright header checks. It's also executed by the git pre-commit hook automatically.
See also: Naming Conventions | File Headers | Version Control