====== Code Conventions ======
Mandatory coding standards for the WvdS FPC RAD Suite.
===== Naming Conventions =====
==== Types ====
^ Category ^ Prefix ^ Example ^
| Class | TWvdS* | TWvdSBuildConfig, TWvdSProjectManager |
| Interface | IWvdS* | IWvdSLogger, IWvdSParser |
| Record | TWvdS* | TWvdSBuildResult, TWvdSToolPath |
| Enum | TWvdS* | TWvdSProjectType, TWvdSBuildStatus |
| Exception | EWvdS* | EWvdSFileNotFound, EWvdSParseError |
| Callback | TWvdS* | TWvdSBuildCallback, TWvdSProgressHandler |
External API types (VSCode, Node.js) retain their original names.
==== Unit Names ====
Microsoft-style namespaces with WvdS prefix:
WvdS.{Domain}.{Layer}.pas
Examples:
WvdS.Build.Models.pas
WvdS.Build.Service.pas
WvdS.Projects.SettingsDialog.pas
WvdS.VSCode.Commands.pas
==== Layer Suffixes ====
^ Suffix ^ Content ^ Example ^
| .Models | Records, enums, types | WvdS.Build.Models |
| .Service | Business logic | WvdS.Build.Service |
| .Dialog | WebView dialogs | WvdS.Projects.SettingsDialog |
| .Provider | VSCode API wrapper | WvdS.Designer.EditorProvider |
==== Variables and Parameters ====
^ Category ^ Prefix ^ Example ^
| Private fields | F | FProjectPath, FConfig |
| Parameters | A | APath, AOptions, ACallback |
| Local variables | None | Result, I, Config |
==== Resourcestrings ====
Prefix by feature:
^ Prefix ^ Feature ^
| rsCore* | Core Extension |
| rsBuild* | Build Extension |
| rsProject* | Projects Extension |
| rsDesigner* | UI Designer |
| rsPreview* | UI Preview |
| rsMeta* | UI Meta |
| rsPackaging* | Packaging |
| rsTool* | Toolchain |
===== Code Structure =====
==== Unit Layout ====
unit WvdS.{Feature}.{Layer};
{$mode objfpc}{$H+}
interface
uses
// 1. System Units
SysUtils, Classes,
// 2. WvdS Common
WvdS.System, WvdS.Collections,
// 3. Feature-specific
WvdS.{Feature}.Models;
type
// Type definitions
function PublicFunction(const AParam: string): Boolean;
procedure PublicProcedure(AValue: Integer);
implementation
uses
// Private uses (units needed only here)
WvdS.VSCode.Strings;
// Private types and variables
type
TInternalHelper = class
end;
var
InternalState: TObject;
// Implementations
function PublicFunction(const AParam: string): Boolean;
begin
// ...
end;
procedure PublicProcedure(AValue: Integer);
begin
// ...
end;
initialization
// Initialization
finalization
// Cleanup
end.
==== Class Layout ====
type
TWvdSExampleClass = class(TObject)
private
FName: string;
FValue: Integer;
procedure SetName(const AValue: string);
protected
procedure DoInternalWork; virtual;
public
constructor Create(const AName: string);
destructor Destroy; override;
procedure Execute;
property Name: string read FName write SetName;
property Value: Integer read FValue write FValue;
end;
===== Documentation =====
==== PasDoc Format ====
(*
@abstract(Short description in one sentence.)
Detailed description of purpose and usage.
Can span multiple sentences.
@param(APath Full path to the file)
@param(AOptions Optional configuration, can be nil)
@returns(True if successful, False on error)
@raises(EWvdSFileNotFound when file does not exist)
@raises(EWvdSAccessDenied when no read permissions)
Security:
- CWE-22: Path is validated against path traversal
@seealso(RelatedFunction)
@seealso(TWvdSRelatedClass)
*)
function ProcessFile(const APath: string; AOptions: TOptions): Boolean;
==== Inline Comments ====
// Short comment for single line
Result := CalculateValue;
// Multi-line comment for more complex logic
// Explains the why, not the what
if (Value > Threshold) and (Mode = mAdvanced) then
begin
// We need to use the advanced algorithm here,
// as the simple one becomes inaccurate for large values
Result := AdvancedCalculation(Value);
end;
===== Constants Instead of Magic Numbers =====
// FORBIDDEN
if Length(Name) > 64 then
if Timeout > 30000 then
// CORRECT
const
MAX_PROJECT_NAME_LENGTH = 64;
DEFAULT_TIMEOUT_MS = 30000;
if Length(Name) > MAX_PROJECT_NAME_LENGTH then
if Timeout > DEFAULT_TIMEOUT_MS then
===== Terminology =====
Use consistent terms:
^ Use ^ Avoid ^
| Path | Url, Location, Dir (inconsistent) |
| Config | Settings, Options, Prefs (inconsistent) |
| Create | Make, Build (for objects) |
| Generate | Create (for output) |
| Validate | Check, Verify (for inputs) |
| Initialize | Setup, Init (inconsistent) |
| Execute | Run, Process (inconsistent) |
===== Formatting =====
==== Indentation ====
* **2 Spaces** for indentation (not tabs)
* **begin** on its own line for blocks
* **end** at the same indentation level as the corresponding begin
// CORRECT
procedure Example;
begin
if Condition then
begin
DoSomething;
DoMore;
end
else
begin
DoAlternative;
end;
end;
// FORBIDDEN
procedure Example; begin
if Condition then begin DoSomething; DoMore; end
else DoAlternative;
end;
==== Line Length ====
* Maximum **120 characters** per line
* Wrap long expressions
// For long parameter lists
function VeryLongFunctionName(
const AFirstParameter: string;
const ASecondParameter: Integer;
AThirdParameter: TOptions
): Boolean;
// For long conditions
if (FirstCondition) and
(SecondCondition) and
(ThirdCondition) then
begin
// ...
end;
===== Forbidden Patterns =====
==== Empty Exception Handlers ====
// FORBIDDEN
try
DoSomething;
except
// Do nothing - swallowing errors!
end;
// CORRECT
try
DoSomething;
except
on E: Exception do
LogError(rsUnexpectedError, [E.ClassName, E.Message]);
end;
==== TODO/FIXME in Production Code ====
// FORBIDDEN in main branch
// TODO: Implement this later
// FIXME: This is broken
// ALLOWED only in feature branches, must be removed before merge
==== Hardcoded Strings ====
// FORBIDDEN
ShowMessage('File not found');
ShowMessage('Datei nicht gefunden');
// CORRECT
ShowMessage(rsFileNotFound); // resourcestring
==== Global Variables in Services ====
// FORBIDDEN
var
GlobalConfig: TConfig; // Hard to test!
// CORRECT - Pass parameters
function ProcessWithConfig(const AConfig: TConfig): Boolean;
===== Testability =====
Services must be testable without VSCode/UI:
// FORBIDDEN - UI in Service
procedure ValidateAndShowError(const AName: string);
begin
if not IsValid(AName) then
ShowMessage('Invalid name'); // UI dependency!
end;
// CORRECT - Return value
function ValidateName(const AName: string; out AError: string): Boolean;
begin
Result := IsValid(AName);
if not Result then
AError := rsInvalidName;
end;
===== pas2js Compatibility (IMPORTANT) =====
**pas2js is NOT 100% compatible with FPC!** These restrictions MUST be observed.
==== Unsupported Features ====
^ Feature ^ FPC ^ pas2js ^ Workaround ^
| ''class var'' | Yes | No | Use unit-level variable |
| Inline ''var'' | Yes | No | Declare in ''var'' block |
| ''Int64'' | Yes | No | Use ''Integer'' |
| ''//'' in ''asm'' | Yes | No | Use ''/* */'' for JS comments |
| Local var in ''asm'' | Yes | No | Use JS objects directly |
| Constants in ''asm'' | Yes | No | Use literal values |
| ''-O2'' + asm calls | Yes | No | Use ''-O-'' or make methods public |
| Unit function in ''asm'' | Yes | No | Use ''pas["Unit.Name"].FuncName()'' |
| ''out'' param in ''asm'' | Yes | No | Use ''Param.set(value)'' |
==== Examples: Types and Syntax ====
(* FORBIDDEN - class var *)
TMyClass = class
class var FInstance: TMyClass; (* pas2js Error! *)
end;
(* CORRECT - Unit-level variable *)
var
GMyInstance: TMyClass = nil;
(* FORBIDDEN - Inline var *)
begin
var X := 42; (* pas2js Error! *)
end;
(* CORRECT - var block *)
var
X: Integer;
begin
X := 42;
end;
(* FORBIDDEN - Int64 *)
var
Size: Int64; (* pas2js: "Symbol Int64 is not implemented" *)
(* CORRECT - Integer *)
var
Size: Integer;
(* FORBIDDEN - // in asm *)
asm
// JavaScript code (* pas2js Error! *)
end;
(* CORRECT - /* */ in asm *)
asm
/* JavaScript code */
end;
==== Examples: asm Block Pitfalls ====
These errors are hard to find as they only occur at runtime!
**Local variables are NOT visible in asm:**
(* FORBIDDEN - Local variable in asm *)
procedure Test;
var
MyVar: TJSObject;
begin
asm
MyVar = {}; (* pas2js Error: MyVar is not defined! *)
end;
end;
(* CORRECT - Use JS objects directly *)
procedure Test;
begin
asm
module.exports.myFunc = function() {}; /* Access directly */
end;
end;
**Constants are NOT visible in asm:**
(* FORBIDDEN - Constant in asm *)
const
TIMEOUT_MS = 5000;
begin
asm
setTimeout(func, TIMEOUT_MS); (* pas2js Error: TIMEOUT_MS is not defined! *)
end;
end;
(* CORRECT - Use literal value *)
begin
asm
setTimeout(func, 5000); /* Literal instead of constant */
end;
end;
**Unit functions - do NOT call with underscores:**
(* FORBIDDEN - self.* or underscore syntax *)
procedure TMyClass.HandleBrowse;
begin
asm
self.ToolchainConfig().DoSomething(); (* pas2js Error! *)
pas.Toolchain_ConfigService.ToolchainConfig(); (* WRONG: Underscores! *)
end;
end;
(* CORRECT - pas["Unit.Name"].FuncName() syntax *)
procedure TMyClass.HandleBrowse;
begin
asm
pas["Toolchain.ConfigService"].ToolchainConfig().DoSomething(); /* Correct! */
end;
end;
**out parameters - do NOT assign directly:**
(* FORBIDDEN - Assign out parameter directly *)
function ValidateMessage(AMsg: TJSObject; out AType: string): Boolean;
begin
asm
AType = AMsg.type; (* WRONG: overwrites getter/setter object! *)
end;
end;
(* CORRECT - Assign out parameter with .set() *)
function ValidateMessage(AMsg: TJSObject; out AType: string): Boolean;
begin
asm
AType.set(AMsg.type); /* CORRECT: calls the setter */
end;
end;
**-O2 optimization - can break asm calls:**
# PROBLEM: With -O2 private methods are optimized away
pas2js -O2 extension_main.pas (* asm calls may fail *)
# SOLUTION 1: Disable optimization
pas2js -O- extension_main.pas
# SOLUTION 2: Make methods public (not recommended)
==== Node.js API Pattern ====
**TNJ* classes are NOT classes** - they are wrappers for functions:
(* FORBIDDEN - Class methods *)
Path := TNJPath.resolve(APath); (* Error! *)
Exists := TNJFS.existsSync(APath); (* Error! *)
Bytes := TNJCrypto.randomBytes(16); (* Error! *)
(* CORRECT - Standalone functions *)
uses NodeJS.Path, NodeJS.FS, NodeJS.Crypto;
Path := PathResolve([APath]); (* Function from NodeJS.Path *)
Exists := ExistsSync(APath); (* Function from NodeJS.FS *)
Bytes := RandomBytes(16); (* Function from NodeJS.Crypto *)
==== TJSPromise Catch Method ====
(* FORBIDDEN - underscore catch *)
MyPromise._catch(ErrorHandler); (* Does NOT work! *)
(* CORRECT - dot catch *)
MyPromise.catch(ErrorHandler); (* This is correct *)
==== Chained Bracket Access ====
pas2js does NOT allow chained bracket access on JSValue:
(* FORBIDDEN - Chained brackets *)
function GetStdout(RawResult: TJSObject): string;
begin
asm
Result = RawResult['stdout']['toString'](); (* pas2js Error! *)
end;
end;
(* CORRECT - Use intermediate variable *)
function GetStdout(RawResult: TJSObject): string;
var
StdoutObj: TJSObject;
begin
StdoutObj := TJSObject(RawResult['stdout']);
asm
Result = StdoutObj['toString'](); /* Works */
end;
end;
**Compiler error:** ''illegal qualifier "[" after "JSValue"''
==== Check Constructor Signatures ====
Constructors have specific signatures - don't guess:
(* FORBIDDEN - Wrong parameter type *)
var
Channel: TOutputChannel;
Logger: TWvdSLogger;
begin
Channel := CreateOutputChannel('WvdS Projects');
Logger := TWvdSLogger.Create(Channel); (* Error: Got TOutputChannel, expected String *)
end;
(* CORRECT - Check signature *)
var
Logger: TWvdSLogger;
begin
Logger := TWvdSLogger.Create('wvds.projects'); (* Source name as string *)
end;
On compiler errors about parameter types, always check the unit declaration.
==== Unit References in asm Blocks ====
pas2js converts dots to underscores:
(* Pascal *)
uses WvdS.Build.Service;
(* In asm block *)
asm
var service = pas.WvdS_Build_Service; /* Underscores! */
end;
==== JavaScript Interop ====
(* asm block for direct JavaScript *)
asm
console.log('Direct JS call');
end;
(* Typed variables for JS objects *)
var
JsObj: TJSObject;
JsArr: TJSArray;
==== Build Configuration ====
Two config files per extension:
**1. Shared Config** (''~/sources/common/wvds_common.pas2js.cfg''):
* Compiler mode, target, defines
* RTL paths, common unit paths
**2. Extension-specific Config** (''build.cfg''):
* Local unit paths (''-Fupas'')
* Output path (''-FEdist/'', ''-odist/extension_main.js'')
Invocation:
pas2js @../../common/wvds_common.pas2js.cfg @build.cfg pas/extension_main.pas
===== SSOT - Use Common Libraries =====
**Direct require() calls are FORBIDDEN!** Always use the Pascal wrappers from ''~/sources/common/''.
==== Node.js Wrappers ====
Available in ''~/sources/common/web/nodejs/'':
^ Unit ^ Purpose ^ Important Functions ^
| ''NodeJS.Base'' | Core Node.js types | TJSObject, RequireModule |
| ''NodeJS.FS'' | File system | ExistsSync, StatSync, ReadFileSync, WriteFileSync |
| ''NodeJS.Path'' | Path manipulation | PathJoin, PathDirname, PathResolve, PathBasename |
| ''NodeJS.ChildProcess'' | Start processes | Spawn, Exec, SpawnSync, TSpawnOptions |
| ''NodeJS.Crypto'' | Cryptography | RandomBytes |
| ''NodeJS.Buffer'' | Binary data | TBuffer |
==== VSCode Wrappers ====
Available in ''~/sources/common/web/vscode/'':
^ Unit ^ Purpose ^ Important Functions ^
| ''VSCode.Base'' | Core VSCode types | TUri, TPosition, TRange, TTextDocument |
| ''VSCode.Window'' | Window operations | ShowInformationMessage, CreateWebviewPanel |
| ''VSCode.Commands'' | Command registration | RegisterCommand, ExecuteCommand |
| ''VSCode.Workspace'' | Workspace operations | FindFiles, GetConfiguration |
| ''VSCode.Tasks'' | Task execution | CreateShellTask, ExecuteTask |
==== WvdS Core System ====
Available in ''~/sources/common/core/system/'':
^ Unit ^ Purpose ^ Important Functions ^
| ''WvdS.System.Logging'' | Unified logging | TWvdSLogger, LogDebug, LogInfo, LogError |
| ''WvdS.VSCode.Security'' | Security functions | EscapeHtml, ValidateWebViewMessage, GetSecureNonce |
| ''WvdS.VSCode.Strings'' | Resourcestrings | 130+ localized strings |
| ''WvdS.WebView.Bridge'' | Host-WebView communication | THostBridge, TBaseBridge |
==== SSOT Refactoring Pattern ====
**Before (SSOT violation):**
function IsFpcAvailable(const AFpcPath: string): Boolean;
begin
asm
try {
var fs = require('fs'); /* FORBIDDEN: direct require! */
Result = fs.existsSync(AFpcPath);
} catch (e) {
Result = false;
}
end;
end;
**After (correct with common library):**
uses
NodeJS.FS; (* Wrapper from common/ *)
function IsFpcAvailable(const AFpcPath: string): Boolean;
begin
Result := ExistsSync(AFpcPath); (* Pascal function *)
end;
==== Async File Operations ====
**Before (SSOT violation):**
function LoadMetadata(const APath: string): TJSPromise;
begin
asm
Result = new Promise(function(resolve, reject) {
var fs = require('fs'); /* FORBIDDEN */
fs.stat(APath, function(err, stats) { ... });
});
end;
end;
**After (correct with common library):**
uses
NodeJS.FS;
function LoadMetadata(const APath: string): TJSPromise;
begin
Result := Stat(APath)._then(
function(stats: JSValue): JSValue
begin
(* Process stats *)
end
);
end;
===== Checklist Before Commit =====
[ ] Naming conventions followed
[ ] PasDoc documentation present
[ ] No magic numbers
[ ] No TODO/FIXME comments
[ ] No hardcoded strings
[ ] No empty exception handlers
[ ] No global variables in services
[ ] No UI dependencies in services
[ ] Formatting correct (indentation, line length)
[ ] Consistent terminology
[ ] Common libraries instead of direct require()
===== See Also =====
* [[.:sicherheit|Security Guidelines]]
* [[.:i18n|Internationalization]]
* [[.:extension-entwicklung|Extension Development]]