canary/tools/setup_vscode_lua_api.ps1
Eduardo Dantas dd1a95c157
feat: add Lua API documentation and doc generator (#3771)
Add Canary's official Lua API documentation generator and keep the generated Lua API reference synchronized with the C++ Lua binding surface.

Main changes:
- Add LuaApiDocGenerator and LuaBindingScanner to discover Lua classes, methods, globals, constants, parameters, returns, fields, overloads, aliases, source files, and class inheritance from the C++ binding layer.
- Generate docs/lua-api/lua_api.d.lua for Lua Language Server and VSCode IntelliSense.
- Generate docs/lua-api/lua_api.md for human-readable API documentation.
- Generate docs/lua-api/lua_api.json for structured tooling and CI metadata.
- Generate docs/lua-api/lua_api_quality_baseline.json for weak-signature regression tracking.
- Integrate documentation generation into the startup after loadConfigLua, controlled by generateLuaApiDocs and luaApiDocsOutputDirectory.
- Add --generate-lua-api-docs-only so CI can regenerate docs without starting the game server, loading maps, connecting to the database, or running shutdown/save paths.

Generator behavior:
- Force documentation generation in docgen-only mode so CI sync checks cannot silently become no-ops.
- Warn instead of crashing the startup when doc generation fails.
- Write generated files atomically.
- Skip unchanged writes.
- Use deterministic ordering and normalized file endings.
- Keep source paths relative and portable.
- Normalize inferred C++ types into Lua and LuaLS-friendly types.
- Avoid exposing raw C++ types in generated stubs.

LuaLS support:
- Emit LuaLS-compatible annotations, including meta, aliases, classes, fields, overloads, params, returns, inheritance, callable constructors, typed arrays, and operators for supported metamethods.
- Avoid exposing internal metamethods such as __eq, __add, and __gc as normal public Lua methods.
- Add explicit signature overlays with docblocks for APIs where automatic inference is not precise enough.
- Add overlays for high-impact APIs such as Player, Game, Result, db async calls, Actions, TalkActions, Spells, Weapons, MoveEvents, GlobalEvents, CreatureEvents, NpcType, Position, and NetworkMessage.

Editor and documentation:
- Add .luarc.json so LuaLS loads docs/lua-api by default.
- Raise LuaLS preload limits for lua_api.d.lua.
- Exclude generated build, cache, Visual Studio, and vcpkg directories from LuaLS workspace indexing.
- Add tools/setup_vscode_lua_api.ps1 to configure VSCode and LuaLS locally.
- Add docs/systems/lua-api-docgen.md explaining configuration, binding documentation, docblocks, quality baselines, and CI checks.
- Mention the generated Lua API docs from the README and systems index.
- Add Visual Studio project and CMake integration for the generator.

CI and regression protection:
- Add CI sync validation that runs --generate-lua-api-docs-only and checks docs/lua-api for diffs.
- Add tools/check_lua_api_quality.py to compare weak-signature metrics against the committed baseline.
- Add tools/check_lua_api_binding_docs.py to require explicit docblocks when new bindings would generate weak signatures.
- Exclude generated Lua API outputs from Sonar duplication/noise while keeping the generator and tooling checked.

Validation:
- Verified docs/lua-api/lua_api.json parses successfully.
- Verified luac -p passes for docs/lua-api/lua_api.d.lua.
- Verified Lua API binding docs and quality checks pass.
- Verified tools/setup_vscode_lua_api.ps1 -WhatIf succeeds.
- Verified VSCode and LuaLS resolve generated Canary classes and methods from lua_api.d.lua.
- Checked generated docs for stale root-level references, temporary paths, local machine paths, and obvious raw C++ types.

This gives Canary a repeatable source-of-truth pipeline for Lua API documentation, editor IntelliSense, and CI enforcement while keeping the generated docs synchronized with the C++ binding layer.
2026-05-25 14:39:39 -03:00

344 lines
9.2 KiB
PowerShell

<#
.SYNOPSIS
Configures VSCode Lua Language Server to use Canary's generated Lua API stubs.
.DESCRIPTION
Run this script from the repository root. It updates the local
.vscode/settings.json file so LuaLS reads docs/lua-api/lua_api.d.lua through
the workspace library. If the repository has a .luarc.json file, the script also
updates that project configuration because LuaLS gives it priority over VSCode
settings.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$SettingsPath = ".vscode/settings.json",
[string]$LuaRcPath = ".luarc.json",
[string]$LuaApiLibraryPath = '${workspaceFolder}/docs/lua-api',
[string]$LuaRcLibraryPath = "docs/lua-api",
[string[]]$LuaRcIgnoreDirectories = @(
".git",
".vs",
"artifacts",
"build",
"cache",
"cmake-build-*",
"database_backup",
"logs",
"Release",
"RelWithDebInfo",
"Testing",
"vcproj",
"vcpkg_installed"
),
[int]$MinimumPreloadFileSizeKb = 1000
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = "Stop"
function Remove-JsonComments {
param([string]$Text)
$builder = [System.Text.StringBuilder]::new()
$inString = $false
$escape = $false
$index = 0
while ($index -lt $Text.Length) {
$char = $Text[$index]
$next = if ($index + 1 -lt $Text.Length) { $Text[$index + 1] } else { [char]0 }
if (-not $inString -and $char -eq "/" -and $next -eq "/") {
while ($index -lt $Text.Length -and $Text[$index] -ne "`n") {
$index++
}
if ($index -lt $Text.Length) {
[void]$builder.Append($Text[$index])
}
$index++
continue
}
if (-not $inString -and $char -eq "/" -and $next -eq "*") {
$index += 2
while ($index + 1 -lt $Text.Length -and -not ($Text[$index] -eq "*" -and $Text[$index + 1] -eq "/")) {
if ($Text[$index] -eq "`n") {
[void]$builder.Append("`n")
}
$index++
}
$index += 2
continue
}
[void]$builder.Append($char)
if ($char -eq '"' -and -not $escape) {
$inString = -not $inString
}
$escape = $inString -and $char -eq "\" -and -not $escape
if ($char -ne "\") {
$escape = $false
}
$index++
}
return $builder.ToString()
}
function ConvertTo-OrderedHashtable {
param([object]$Value)
if ($null -eq $Value) {
return $null
}
if ($Value -is [string] -or $Value -is [ValueType]) {
return $Value
}
if ($Value -is [System.Collections.IDictionary]) {
$hash = [ordered]@{}
foreach ($key in $Value.Keys) {
$hash[$key] = ConvertTo-OrderedHashtable $Value[$key]
}
return $hash
}
if ($Value -is [System.Collections.IEnumerable]) {
$items = [System.Collections.Generic.List[object]]::new()
foreach ($item in $Value) {
$items.Add((ConvertTo-OrderedHashtable $item))
}
return ,$items.ToArray()
}
$properties = @($Value.PSObject.Properties)
if ($properties.Count -gt 0) {
$hash = [ordered]@{}
foreach ($property in $properties) {
$hash[$property.Name] = ConvertTo-OrderedHashtable $property.Value
}
return $hash
}
return $Value
}
function Get-ArraySetting {
param(
[System.Collections.IDictionary]$Settings,
[string]$Key
)
if (-not $Settings.Contains($Key) -or $null -eq $Settings[$Key]) {
return @()
}
$value = $Settings[$Key]
if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) {
return @($value)
}
return @($value)
}
function Add-UniqueString {
param(
[System.Collections.Generic.List[object]]$List,
[string]$Value
)
foreach ($entry in $List) {
if ($entry -is [string] -and $entry -eq $Value) {
return
}
}
$List.Add($Value)
}
function Remove-LegacyEntries {
param(
[object[]]$Entries,
[string[]]$LegacyPaths
)
$result = [System.Collections.Generic.List[object]]::new()
foreach ($entry in $Entries) {
if ($entry -is [string] -and $LegacyPaths -contains $entry) {
continue
}
$result.Add($entry)
}
return ,$result.ToArray()
}
function Resolve-RepositoryPath {
param(
[string]$RepoRoot,
[string]$Path
)
if ([System.IO.Path]::IsPathRooted($Path)) {
return $Path
}
return Join-Path $RepoRoot $Path
}
function Read-JsonSettings {
param([string]$Path)
if (-not (Test-Path $Path)) {
return [ordered]@{}
}
$rawJson = Get-Content -Raw -Path $Path
if ([string]::IsNullOrWhiteSpace($rawJson)) {
return [ordered]@{}
}
$json = Remove-JsonComments $rawJson
$json = [regex]::Replace($json, ",(\s*[\]}])", '$1')
return ConvertTo-OrderedHashtable ($json | ConvertFrom-Json)
}
function Set-LibrarySetting {
param(
[System.Collections.IDictionary]$Settings,
[string]$Key,
[string]$LibraryPath,
[string[]]$LegacyPaths
)
$libraryEntries = Remove-LegacyEntries (Get-ArraySetting $Settings $Key) $LegacyPaths
$library = [System.Collections.Generic.List[object]]::new()
foreach ($entry in $libraryEntries) {
$library.Add($entry)
}
Add-UniqueString $library $LibraryPath
$Settings[$Key] = @($library)
}
function Set-MinimumIntegerSetting {
param(
[System.Collections.IDictionary]$Settings,
[string]$Key,
[int]$Minimum
)
if (-not $Settings.Contains($Key) -or $null -eq $Settings[$Key]) {
$Settings[$Key] = $Minimum
return
}
$current = 0
if (-not [int]::TryParse([string]$Settings[$Key], [ref]$current) -or $current -lt $Minimum) {
$Settings[$Key] = $Minimum
}
}
function Set-StringArraySetting {
param(
[System.Collections.IDictionary]$Settings,
[string]$Key,
[string[]]$Values
)
$entries = Get-ArraySetting $Settings $Key
$updated = [System.Collections.Generic.List[object]]::new()
foreach ($entry in $entries) {
$updated.Add($entry)
}
foreach ($value in $Values) {
Add-UniqueString $updated $value
}
$Settings[$Key] = @($updated)
}
function Write-JsonSettings {
param(
[System.Collections.IDictionary]$Settings,
[string]$Path
)
$jsonOutput = ($Settings | ConvertTo-Json -Depth 20)
$jsonOutput = $jsonOutput.TrimEnd() + [Environment]::NewLine
Set-Content -Path $Path -Value $jsonOutput -Encoding UTF8
}
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$fullSettingsPath = Resolve-RepositoryPath $repoRoot $SettingsPath
$settingsDirectory = Split-Path -Parent $fullSettingsPath
if (-not (Test-Path $settingsDirectory)) {
New-Item -ItemType Directory -Path $settingsDirectory | Out-Null
}
$settings = Read-JsonSettings $fullSettingsPath
$legacyLibraryPaths = @(
"./docs/lua_api.lua",
"docs/lua_api.lua",
'${workspaceFolder}/docs/lua_api.lua',
"./docs/lua-api/lua_api.lua",
"docs/lua-api/lua_api.lua",
'${workspaceFolder}/docs/lua-api/lua_api.lua'
)
Set-LibrarySetting $settings "Lua.workspace.library" $LuaApiLibraryPath $legacyLibraryPaths
$legacyRuntimePluginPaths = @(
"./docs/lua_api.json",
"docs/lua_api.json",
'${workspaceFolder}/docs/lua_api.json',
"./docs/lua-api/lua_api.json",
"docs/lua-api/lua_api.json",
'${workspaceFolder}/docs/lua-api/lua_api.json'
)
if ($settings.Contains("Lua.runtime.plugin")) {
$pluginEntries = Remove-LegacyEntries (Get-ArraySetting $settings "Lua.runtime.plugin") $legacyRuntimePluginPaths
if ($pluginEntries.Count -gt 0) {
$settings["Lua.runtime.plugin"] = @($pluginEntries)
} else {
$settings.Remove("Lua.runtime.plugin")
}
}
if (-not $settings.Contains("Lua.workspace.checkThirdParty")) {
$settings["Lua.workspace.checkThirdParty"] = $false
}
if ($PSCmdlet.ShouldProcess($fullSettingsPath, "Update VSCode LuaLS settings")) {
Write-JsonSettings $settings $fullSettingsPath
}
$updatedLuaRc = $false
$fullLuaRcPath = Resolve-RepositoryPath $repoRoot $LuaRcPath
if (Test-Path $fullLuaRcPath) {
$luaRcSettings = Read-JsonSettings $fullLuaRcPath
Set-LibrarySetting $luaRcSettings "workspace.library" $LuaRcLibraryPath $legacyLibraryPaths
Set-MinimumIntegerSetting $luaRcSettings "workspace.preloadFileSize" $MinimumPreloadFileSizeKb
Set-StringArraySetting $luaRcSettings "workspace.ignoreDir" $LuaRcIgnoreDirectories
if (-not $luaRcSettings.Contains("workspace.checkThirdParty")) {
$luaRcSettings["workspace.checkThirdParty"] = $false
}
if ($PSCmdlet.ShouldProcess($fullLuaRcPath, "Update LuaLS project settings")) {
Write-JsonSettings $luaRcSettings $fullLuaRcPath
}
$updatedLuaRc = $true
}
Write-Host "VSCode Lua API library configured: $LuaApiLibraryPath"
Write-Host "Settings file: $SettingsPath"
if ($updatedLuaRc) {
Write-Host "LuaLS project config updated: $LuaRcPath"
}
Write-Host "Install the VSCode Lua extension if it is not installed."