====== 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]]