From 636344666da7c616f33e3a028e78df5b5bcdfed7 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 25 Sep 2026 21:21:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(epf-build):=20=D0=BE=D0=B1=D1=89=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20configSrc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка исходников идёт в конфигурации-заглушке, где общих модулей не было: обработка с кодом БСП не собиралась без -Checks off. - stub-db-create -ConfigSrc: общие модули, к которым обращается код, получают пустых двойников с флагами контекста из выгрузки; неизвестные имена не угадываются - epf-build -ConfigSrc, по умолчанию configSrc базы из .v8-project.json - подсказки к «Переменная не определена (X)»: модуль не в том контексте / модуля нет в configSrc / выгрузка не передана (с -ConfigSrc и -Checks off) Co-Authored-By: Claude Opus 5.5 (1M context) --- .claude/skills/epf-build/SKILL.md | 7 + .../skills/epf-build/scripts/epf-build.ps1 | 65 ++++++++- .claude/skills/epf-build/scripts/epf-build.py | 73 +++++++++- .../epf-build/scripts/stub-db-create.ps1 | 95 ++++++++++++- .../epf-build/scripts/stub-db-create.py | 107 ++++++++++++++- .claude/skills/erf-build/SKILL.md | 7 + .../cases/epf-build/check-hint-configsrc.json | 58 ++++++++ .../epf-build/check-hint-no-configsrc.json | 44 ++++++ .../configsrc-common-modules.json | 129 ++++++++++++++++++ 9 files changed, 575 insertions(+), 10 deletions(-) create mode 100644 tests/skills/cases/epf-build/check-hint-configsrc.json create mode 100644 tests/skills/cases/epf-build/check-hint-no-configsrc.json create mode 100644 tests/skills/cases/stub-db-create/configsrc-common-modules.json diff --git a/.claude/skills/epf-build/SKILL.md b/.claude/skills/epf-build/SKILL.md index 9f7d94644..d6212d167 100644 --- a/.claude/skills/epf-build/SKILL.md +++ b/.claude/skills/epf-build/SKILL.md @@ -58,6 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п | `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | | `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` | | `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` | +| `-ConfigSrc <путь>` | нет | Каталог XML-выгрузки конфигурации, в которой будет работать обработка. По умолчанию — `configSrc` указанной базы из `.v8-project.json` | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` | @@ -72,6 +73,12 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает. +Общие модули конфигурации (`ОбщегоНазначения`, `УправлениеПечатью` и т.п.) проверка видит только +при известной выгрузке конфигурации — `-ConfigSrc` или `configSrc` базы в `.v8-project.json`. +Без неё обращения к ним — ошибка «Переменная не определена»; тогда укажи выгрузку или собери +с `-Checks off`. Методы глобальных общих модулей, вызванные без имени модуля, проверка не видит +и с выгрузкой — для такого кода остаётся `-Checks off`. + ## Примеры ```powershell diff --git a/.claude/skills/epf-build/scripts/epf-build.ps1 b/.claude/skills/epf-build/scripts/epf-build.ps1 index 6fe8a3d35..d951d0058 100644 --- a/.claude/skills/epf-build/scripts/epf-build.ps1 +++ b/.claude/skills/epf-build/scripts/epf-build.ps1 @@ -1,4 +1,4 @@ -# epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.20 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -33,6 +33,9 @@ .PARAMETER OutputFile Путь к выходному EPF/ERF-файлу +.PARAMETER ConfigSrc + Каталог XML-выгрузки целевой конфигурации (общие модули для проверки исходников) + .PARAMETER AdditionalV8Arguments Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) @@ -80,6 +83,10 @@ param( # Контексты синтаксической проверки. По умолчанию ThinClient,Server. [Parameter(Mandatory=$false)] [string]$Context, + # Выгрузка конфигурации, в которой будет работать обработка: её общие модули видны проверке. + # Без параметра берётся configSrc базы из .v8-project.json. + [Parameter(Mandatory=$false)] + [string]$ConfigSrc, [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -262,6 +269,7 @@ $V8Path = ConvertTo-CleanPath $V8Path '-V8Path' $InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath' $SourceFile = ConvertTo-CleanPath $SourceFile '-SourceFile' $OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile' +$ConfigSrc = ConvertTo-CleanPath $ConfigSrc '-ConfigSrc' function Assert-InfoBaseExists { # These skills work on a ready infobase. Saying so up front beats the platform's @@ -481,6 +489,24 @@ function Resolve-SourcePath { return $null } +# «Переменная не определена (X)» при обращении X.… — чаще всего общий модуль конфигурации, которого +# проверке не показали. Платформа печатает строку кода следующей строкой лога с меткой <> перед X. +# Подсказка одна на имя; сама ошибка остаётся ошибкой. +function Get-UndefinedModuleHint { + param([string]$Line, [string]$CodeLine, $Hinted) + $m = [regex]::Match($Line, 'Переменная не определена \(([^)]+)\)') + if (-not $m.Success) { return $null } + $name = $m.Groups[1].Value + if (-not [regex]::IsMatch($CodeLine, '<<\?>>\s*' + [regex]::Escape($name) + '\s*\.')) { return $null } + if (-not $Hinted.Add($name)) { return $null } + if (-not $checkConfigSrc) { return "похоже на общий модуль конфигурации — выгрузка конфигурации проверке не передана" } + $cmDir = Join-Path $checkConfigSrc "CommonModules" + $inCfg = (Test-Path -LiteralPath $cmDir -PathType Container) -and + @(Get-ChildItem -LiteralPath $cmDir -Filter "*.xml" -File | Where-Object { $_.BaseName.Equals($name, [StringComparison]::OrdinalIgnoreCase) }).Count -gt 0 + if ($inCfg) { return "общий модуль $name есть в configSrc, но недоступен в контексте проверки (см. «Проверка: …» в строке выше)" } + return "общего модуля $name нет в configSrc ($checkConfigSrc)" +} + # $true, если платформа нашла проблемы — вызывающий не собирает артефакт. function Invoke-SourceCheck { param([string]$Exe, [string]$BasePath, [string[]]$Flags, [string]$SourceDir, [string[]]$ExtraArgs) @@ -508,10 +534,19 @@ function Invoke-SourceCheck { Write-Host "Error: платформа нашла проблемы в исходниках — сборка отменена" -ForegroundColor Red # Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя. if ($lines.Count -eq 0) { Write-Host " платформа вернула код $($res.ExitCode) без сообщений" -ForegroundColor Red } - foreach ($l in $lines) { + $hinted = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) + for ($i = 0; $i -lt $lines.Count; $i++) { + $l = $lines[$i] Write-Host " $($l.TrimEnd())" -ForegroundColor Red $srcPath = Resolve-SourcePath $l $SourceDir if ($srcPath) { Write-Host " -> $srcPath" -ForegroundColor Red } + # Подсказка — под строкой кода, которую платформа печатает следом за ошибкой. + $hint = if ($i -gt 0) { Get-UndefinedModuleHint $lines[$i - 1] $l $hinted } else { $null } + if ($hint) { Write-Host " [hint] $hint" -ForegroundColor Yellow } + } + if ($hinted.Count -gt 0 -and -not $checkConfigSrc) { + Write-Host "[hint] Общие модули конфигурации проверке не видны: $(@($hinted) -join ', ')." -ForegroundColor Yellow + Write-Host " Укажите -ConfigSrc (каталог выгрузки конфигурации) или базу с configSrc в .v8-project.json; либо соберите без проверки: -Checks off" -ForegroundColor Yellow } return $true } finally { @@ -566,6 +601,31 @@ elseif ($checkList.Count -gt 0 -and $checkList -notcontains 'modules') { } $sourceDir = Split-Path $SourceFile -Parent +# Выгрузка целевой конфигурации для проверки: явный -ConfigSrc, иначе configSrc базы из реестра. +# Без неё обращения к общим модулям конфигурации проверка считает неопределёнными переменными. +function Resolve-ConfigSrc { + if ($ConfigSrc) { + if (-not (Test-Path -LiteralPath $ConfigSrc -PathType Container)) { + Write-Host "Error: -ConfigSrc not found: $ConfigSrc" -ForegroundColor Red + exit 1 + } + return (Resolve-Path -LiteralPath $ConfigSrc).Path + } + $db = Find-ProjectDatabase + if (-not $db -or -not $db.configSrc) { return $null } + $p = [string]$db.configSrc + if (-not [System.IO.Path]::IsPathRooted($p)) { + $pf = Find-V8Project (Get-Location).Path + $p = Join-Path (Split-Path $pf -Parent) $p + } + if (-not (Test-Path -LiteralPath $p -PathType Container)) { + Write-Host "WARNING: configSrc базы не найден: $p — общие модули конфигурации проверке недоступны" -ForegroundColor Yellow + return $null + } + return $p +} +$checkConfigSrc = if ($checkList.Count -gt 0) { Resolve-ConfigSrc } else { $null } + function New-StubBase { # Стаб запускает свои процессы платформы (CREATEINFOBASE, LoadConfigFromFiles, UpdateDBCfg) — # им нужны те же дополнительные аргументы, что и сборке. Передаются только явные: файл проекта @@ -576,6 +636,7 @@ function New-StubBase { $q = { param($s) "'" + ($s -replace "'", "''") + "'" } $stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $BasePath)" if ($Embed) { $stubCmd += " -EmbedSourceFile $(& $q $SourceFile)" } + if ($Embed -and $checkConfigSrc) { $stubCmd += " -ConfigSrc $(& $q $checkConfigSrc)" } if ($AdditionalV8Arguments.Count -gt 0) { $stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',') } diff --git a/.claude/skills/epf-build/scripts/epf-build.py b/.claude/skills/epf-build/scripts/epf-build.py index c91937495..58052cfff 100644 --- a/.claude/skills/epf-build/scripts/epf-build.py +++ b/.claude/skills/epf-build/scripts/epf-build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.20 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -513,7 +513,7 @@ def resolve_source_path(line, source_dir): # True, если платформа нашла проблемы — вызывающий не собирает артефакт. -def invoke_source_check(exe, base_path, flags, source_dir, extra_args): +def invoke_source_check(exe, base_path, flags, source_dir, extra_args, config_src=None): exe_dir = os.path.dirname(exe) exe_leaf = os.path.basename(exe) if exe_leaf.lower().startswith('ibcmd'): @@ -547,16 +547,73 @@ def invoke_source_check(exe, base_path, flags, source_dir, extra_args): # Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя. if not lines: print(f' платформа вернула код {result.returncode} без сообщений') - for l in lines: + hinted = [] + for i, l in enumerate(lines): print(f' {l.rstrip()}') src_path = resolve_source_path(l, source_dir) if src_path: print(f' -> {src_path}') + # Подсказка — под строкой кода, которую платформа печатает следом за ошибкой. + hint = get_undefined_module_hint(lines[i - 1], l, hinted, config_src) if i > 0 else None + if hint: + print(f' [hint] {hint}') + if hinted and not config_src: + print('[hint] \u041e\u0431\u0449\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 \u043d\u0435 \u0432\u0438\u0434\u043d\u044b: %s.' % ', '.join(hinted)) + print(' \u0423\u043a\u0430\u0436\u0438\u0442\u0435 -ConfigSrc (\u043a\u0430\u0442\u0430\u043b\u043e\u0433 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438) \u0438\u043b\u0438 \u0431\u0430\u0437\u0443 \u0441 configSrc \u0432 .v8-project.json; \u043b\u0438\u0431\u043e \u0441\u043e\u0431\u0435\u0440\u0438\u0442\u0435 \u0431\u0435\u0437 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438: -Checks off') return True finally: shutil.rmtree(d, ignore_errors=True) +# Выгрузка целевой конфигурации для проверки: явный -ConfigSrc, иначе configSrc базы из реестра. +# Без неё обращения к общим модулям конфигурации проверка считает неопределёнными переменными. +def resolve_config_src(args): + if args.ConfigSrc: + if not os.path.isdir(args.ConfigSrc): + print('Error: -ConfigSrc not found: %s' % args.ConfigSrc) + sys.exit(1) + return os.path.abspath(args.ConfigSrc) + db = find_project_database(args) + if not db or not db.get('configSrc'): + return None + p = str(db['configSrc']) + # Реестр пишут и на Windows: относительный путь может прийти с обратными слешами. + if os.sep == '/': + p = p.replace('\\', '/') + if not os.path.isabs(p): + pf = _sg_find_v8project(os.getcwd()) + p = os.path.join(os.path.dirname(pf), p) + if not os.path.isdir(p): + print('WARNING: configSrc \u0431\u0430\u0437\u044b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d: %s \u2014 \u043e\u0431\u0449\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b' % p) + return None + return p + + +# «Переменная не определена (X)» при обращении X.… — чаще всего общий модуль конфигурации, которого +# проверке не показали. Платформа печатает строку кода следующей строкой лога с меткой <> перед X. +# Подсказка одна на имя; сама ошибка остаётся ошибкой. +def get_undefined_module_hint(line, code_line, hinted, config_src): + m = re.search(r'\u041f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043d\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430 \(([^)]+)\)', line) + if not m: + return None + name = m.group(1) + if not re.search(r'<<\?>>\s*' + re.escape(name) + r'\s*\.', code_line): + return None + if any(h.casefold() == name.casefold() for h in hinted): + return None + hinted.append(name) + if not config_src: + return '\u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u043e\u0431\u0449\u0438\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u2014 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0430' + cm_dir = os.path.join(config_src, 'CommonModules') + in_cfg = os.path.isdir(cm_dir) and any( + f.lower().endswith('.xml') and os.path.splitext(f)[0].casefold() == name.casefold() + and os.path.isfile(os.path.join(cm_dir, f)) + for f in os.listdir(cm_dir)) + if in_cfg: + return '\u043e\u0431\u0449\u0438\u0439 \u043c\u043e\u0434\u0443\u043b\u044c %s \u0435\u0441\u0442\u044c \u0432 configSrc, \u043d\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0432 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 (\u0441\u043c. \u00ab\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430: \u2026\u00bb \u0432 \u0441\u0442\u0440\u043e\u043a\u0435 \u0432\u044b\u0448\u0435)' % name + return '\u043e\u0431\u0449\u0435\u0433\u043e \u043c\u043e\u0434\u0443\u043b\u044f %s \u043d\u0435\u0442 \u0432 configSrc (%s)' % (name, config_src) + + # Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck. def get_source_check_list(checks): known = ['modules', 'handlers', 'unreferenced', 'empty-handlers', 'config'] @@ -601,6 +658,9 @@ def main(): parser.add_argument("-Checks", default="") # Контексты синтаксической проверки. По умолчанию ThinClient,Server. parser.add_argument("-Context", default="") + # Выгрузка конфигурации, в которой будет работать обработка: её общие модули видны проверке. + # Без параметра берётся configSrc базы из .v8-project.json. + parser.add_argument("-ConfigSrc", default="") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -614,6 +674,7 @@ def main(): assert_infobase_exists(args.InfoBasePath) args.SourceFile = clean_path(args.SourceFile, "-SourceFile") args.OutputFile = clean_path(args.OutputFile, "-OutputFile") + args.ConfigSrc = clean_path(args.ConfigSrc, "-ConfigSrc") # --- Resolve V8Path --- v8path = resolve_v8path(args.V8Path, args) @@ -643,6 +704,7 @@ def main(): print('Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней') sys.exit(1) source_dir = os.path.dirname(os.path.abspath(args.SourceFile)) + check_config_src = resolve_config_src(args) if check_list else None def new_stub_base(base_path, embed): # The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles, @@ -653,6 +715,8 @@ def main(): "-TempBasePath", base_path] if embed: stub_cmd += ["-EmbedSourceFile", args.SourceFile] + if embed and check_config_src: + stub_cmd += ["-ConfigSrc", check_config_src] if v8_extra: stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra) if ibcmd_extra: @@ -700,7 +764,8 @@ def main(): # Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём. check_extra = [] if engine == "ibcmd" else extra_args found = invoke_source_check(v8path, check_base_path, - get_check_flags(check_list, context_list), source_dir, check_extra) + get_check_flags(check_list, context_list), source_dir, check_extra, + check_config_src) if found: if auto_created_base and os.path.exists(auto_created_base): shutil.rmtree(auto_created_base, ignore_errors=True) diff --git a/.claude/skills/epf-build/scripts/stub-db-create.ps1 b/.claude/skills/epf-build/scripts/stub-db-create.ps1 index 85da57924..dbc05a8b1 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.ps1 +++ b/.claude/skills/epf-build/scripts/stub-db-create.ps1 @@ -1,4 +1,4 @@ -# stub-db-create v1.11 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.12 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -13,6 +13,10 @@ param( # смогла проверить его штатными проверками. Без параметра стаб работает как раньше. [string]$EmbedSourceFile, + # Выгрузка целевой конфигурации: общие модули, к которым обращается код, получают в заглушке + # пустых двойников с настоящими флагами контекста. + [string]$ConfigSrc, + [string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalIbcmdArguments = @() @@ -43,6 +47,7 @@ function ConvertTo-CleanPath { $SourceDir = ConvertTo-CleanPath $SourceDir '-SourceDir' $V8Path = ConvertTo-CleanPath $V8Path '-V8Path' $TempBasePath = ConvertTo-CleanPath $TempBasePath '-TempBasePath' +$ConfigSrc = ConvertTo-CleanPath $ConfigSrc '-ConfigSrc' # --- Additional platform arguments --- $script:V8OwnedKeys = @( @@ -353,10 +358,67 @@ foreach ($f in $xmlFiles) { } } +# --- 1c. Общие модули целевой конфигурации, к которым обращается код --- +# Проверка модулей (замер 8.3.24, 8.3.27) требует от общего модуля только двух вещей: чтобы он +# существовал и был доступен в контексте вызова. Методы, их экспорт и число параметров она не +# сверяет. Поэтому двойнику хватает имени и флагов контекста из настоящей выгрузки, тело пустое, +# и зависимости модуля за ним не тянутся. Двойник получает только имя, которое есть в выгрузке: +# неизвестное имя остаётся ошибкой проверки, а не угадывается. + +# Код без строковых литералов и комментариев: слова в них — не обращения к модулям. +function Remove-BslNoise { + param([string]$Code) + $out = New-Object System.Text.StringBuilder + foreach ($line in ($Code -split "`r?`n")) { + # Продолжение многострочной строки («|ВЫБРАТЬ …»): литерал до закрывающей кавычки, после + # неё может идти код («|ГДЕ …"; Х = Модуль.Метод();»). + $l = [regex]::Replace($line, '^\s*\|(?:[^"]|"")*("|$)', '""') + # Литерал до закрывающей кавычки или, если строка продолжится ниже, до конца строки. + $l = [regex]::Replace($l, '"(?:[^"]|"")*("|$)', '""') + $ci = $l.IndexOf('//') + if ($ci -ge 0) { $l = $l.Substring(0, $ci) } + [void]$out.AppendLine($l) + } + return $out.ToString() +} + +$commonModuleFlags = @('Global', 'ClientManagedApplication', 'Server', 'ExternalConnection', 'ClientOrdinaryApplication', 'ServerCall', 'Privileged') +$commonModules = [ordered]@{} # имя из выгрузки -> @{ флаг = 'true'|'false' } +if ($ConfigSrc) { + $cmDir = Join-Path $ConfigSrc "CommonModules" + if (-not (Test-Path -LiteralPath $cmDir -PathType Container)) { + Write-Host "WARNING: в -ConfigSrc нет каталога CommonModules: $ConfigSrc" -ForegroundColor Yellow + } else { + # Идентификаторы 1С регистронезависимы — и имена модулей в коде тоже. + $known = New-Object 'System.Collections.Generic.Dictionary[string,string]' ([StringComparer]::OrdinalIgnoreCase) + foreach ($f in (Get-ChildItem -LiteralPath $cmDir -Filter "*.xml" -File)) { $known[$f.BaseName] = $f.FullName } + $used = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) + foreach ($f in (Get-ChildItem -LiteralPath $SourceDir -Filter "*.bsl" -Recurse -File)) { + $code = Remove-BslNoise ([System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)) + foreach ($m in [regex]::Matches($code, '(?(true|false)") { $Matches[1].ToLower() } else { 'false' } + } + $commonModules[$n] = $flags + } + if ($commonModules.Count -gt 0) { + Write-Host "Общие модули из configSrc: $(@($commonModules.Keys) -join ', ')" + } else { + Write-Host "Общие модули из configSrc: не понадобились" + } + } +} + $hasRefTypes = $typeMap.Count -gt 0 # Конфигурация нужна и тогда, когда ссылочных типов нет: в неё кладётся сам объект. $embedRequested = -not [string]::IsNullOrWhiteSpace($EmbedSourceFile) -$needCfg = $hasRefTypes -or $embedRequested +$needCfg = $hasRefTypes -or $embedRequested -or $commonModules.Count -gt 0 # --- 2. Determine TempBasePath --- if (-not $TempBasePath) { @@ -673,6 +735,7 @@ if ($needCfg) { # ChildObjects entries $childXml = "`r`n`t`t`tРусский" + foreach ($cmName in $commonModules.Keys) { $childXml += "`r`n`t`t`t$cmName" } foreach ($metaType in $typeMap.Keys) { if (-not $metaInfo.ContainsKey($metaType)) { continue } $tag = $metaInfo[$metaType].tag @@ -779,6 +842,34 @@ if ($needCfg) { "@ [System.IO.File]::WriteAllText((Join-Path $langDir "Русский.xml"), $langXml, $enc) + # --- 4b'. Common modules: пустые двойники с флагами контекста из выгрузки --- + if ($commonModules.Count -gt 0) { + $cmOutDir = Join-Path $cfgDir "CommonModules" + New-Item -ItemType Directory -Path $cmOutDir -Force | Out-Null + foreach ($cmName in $commonModules.Keys) { + $flags = $commonModules[$cmName] + $flagXml = "" + foreach ($fl in $commonModuleFlags) { $flagXml += "`r`n`t`t`t<$fl>$($flags[$fl])" } + $cmXml = @" + + + + + $cmName + + $flagXml + DontUse + + + +"@ + [System.IO.File]::WriteAllText((Join-Path $cmOutDir "$cmName.xml"), $cmXml, $enc) + $cmExt = Join-Path $cmOutDir (Join-Path $cmName "Ext") + New-Item -ItemType Directory -Path $cmExt -Force | Out-Null + [System.IO.File]::WriteAllText((Join-Path $cmExt "Module.bsl"), "", $enc) + } + } + # --- 4c. Metadata object stubs --- foreach ($metaType in $typeMap.Keys) { if (-not $metaInfo.ContainsKey($metaType)) { continue } diff --git a/.claude/skills/epf-build/scripts/stub-db-create.py b/.claude/skills/epf-build/scripts/stub-db-create.py index 89548f798..00472e758 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.py +++ b/.claude/skills/epf-build/scripts/stub-db-create.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# stub-db-create v1.11 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.12 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -1216,6 +1216,100 @@ def add_source_object_to_config(source_xml, cfg_dir): return {'tag': cfg_tag, 'name': name} +# --- Общие модули целевой конфигурации, к которым обращается код --- +# Проверка модулей (замер 8.3.24, 8.3.27) требует от общего модуля только двух вещей: чтобы он +# существовал и был доступен в контексте вызова. Методы, их экспорт и число параметров она не +# сверяет. Поэтому двойнику хватает имени и флагов контекста из настоящей выгрузки, тело пустое, +# и зависимости модуля за ним не тянутся. Двойник получает только имя, которое есть в выгрузке: +# неизвестное имя остаётся ошибкой проверки, а не угадывается. + +COMMON_MODULE_FLAGS = ['Global', 'ClientManagedApplication', 'Server', 'ExternalConnection', + 'ClientOrdinaryApplication', 'ServerCall', 'Privileged'] + + +def remove_bsl_noise(code): + # Код без строковых литералов и комментариев: слова в них — не обращения к модулям. + out = [] + for line in re.split(r'\r?\n', code): + # Продолжение многострочной строки («|ВЫБРАТЬ …»): литерал до закрывающей кавычки, после + # неё может идти код («|ГДЕ …"; Х = Модуль.Метод();»). + line = re.sub(r'^\s*\|(?:[^"]|"")*("|$)', '""', line) + # Литерал до закрывающей кавычки или, если строка продолжится ниже, до конца строки. + line = re.sub(r'"(?:[^"]|"")*("|$)', '""', line) + ci = line.find('//') + if ci >= 0: + line = line[:ci] + out.append(line) + return '\n'.join(out) + '\n' + + +def scan_common_modules(source_dir, config_src): + """\u0418\u043c\u044f \u0438\u0437 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 -> {\u0444\u043b\u0430\u0433: 'true'|'false'} \u0434\u043b\u044f \u043e\u0431\u0449\u0438\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439, \u043a \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043e\u0431\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u0434.""" + result = {} + if not config_src: + return result + cm_dir = os.path.join(config_src, 'CommonModules') + if not os.path.isdir(cm_dir): + print('WARNING: \u0432 -ConfigSrc \u043d\u0435\u0442 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0430 CommonModules: %s' % config_src) + return result + # Идентификаторы 1С регистронезависимы — и имена модулей в коде тоже. + known = {} + for fn in os.listdir(cm_dir): + full = os.path.join(cm_dir, fn) + if fn.lower().endswith('.xml') and os.path.isfile(full): + known[os.path.splitext(fn)[0].casefold()] = full + used = set() + for root, _dirs, files in os.walk(source_dir): + for fn in files: + if not fn.lower().endswith('.bsl'): + continue + # Битые байты (файл не в UTF-8) заменяются, как в PS-мастере, а не валят заглушку. + with open(os.path.join(root, fn), 'r', encoding='utf-8-sig', errors='replace') as fh: + code = remove_bsl_noise(fh.read()) + for m in re.finditer(r'(?(true|false)' % (fl, fl), t, re.IGNORECASE) + flags[fl] = m.group(1).lower() if m else 'false' + result[n] = flags + if result: + print('\u041e\u0431\u0449\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u0438\u0437 configSrc: %s' % ', '.join(result.keys())) + else: + print('\u041e\u0431\u0449\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u0438 \u0438\u0437 configSrc: \u043d\u0435 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u043b\u0438\u0441\u044c') + return result + + +def write_common_module_stubs(cfg_dir, ns_decl, common_modules): + # Пустые двойники с флагами контекста из выгрузки. + if not common_modules: + return + cm_out = os.path.join(cfg_dir, 'CommonModules') + os.makedirs(cm_out, exist_ok=True) + for name, flags in common_modules.items(): + flag_xml = ''.join('\n\t\t\t<%s>%s' % (fl, flags[fl], fl) for fl in COMMON_MODULE_FLAGS) + cm_xml = f""" + +\t +\t\t +\t\t\t{name} +\t\t\t +\t\t\t{flag_xml} +\t\t\tDontUse +\t\t +\t + +""" + write_bom(os.path.join(cm_out, name + '.xml'), cm_xml) + ext_dir = os.path.join(cm_out, name, 'Ext') + os.makedirs(ext_dir, exist_ok=True) + write_bom(os.path.join(ext_dir, 'Module.bsl'), '') + + def main(): sys.stdout.reconfigure(encoding='utf-8') sys.stderr.reconfigure(encoding='utf-8') @@ -1227,6 +1321,9 @@ def main(): # XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа # смогла проверить его штатными проверками. Без параметра стаб работает как раньше. parser.add_argument('-EmbedSourceFile', default='') + # Выгрузка целевой конфигурации: общие модули, к которым обращается код, получают в заглушке + # пустых двойников с настоящими флагами контекста. + parser.add_argument('-ConfigSrc', default='') parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[], help='Extra 1cv8 arguments, e.g. /UseHwLicenses+') parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[], @@ -1239,12 +1336,14 @@ def main(): args.V8Path = clean_path(args.V8Path, "-V8Path") args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath") args.EmbedSourceFile = clean_path(args.EmbedSourceFile, "-EmbedSourceFile") + args.ConfigSrc = clean_path(args.ConfigSrc, "-ConfigSrc") type_map = scan_ref_types(args.SourceDir) register_columns = scan_register_columns(args.SourceDir) + common_modules = scan_common_modules(args.SourceDir, args.ConfigSrc) has_ref_types = len(type_map) > 0 embed_requested = bool(args.EmbedSourceFile and args.EmbedSourceFile.strip()) - need_cfg = has_ref_types or embed_requested + need_cfg = has_ref_types or embed_requested or len(common_modules) > 0 stub_format_version = detect_stub_format_version(args.SourceDir) stub_compat = stub_compatibility_mode(stub_format_version) ns_decl = f'{NS} version="{stub_format_version}"' @@ -1279,6 +1378,8 @@ def main(): co_xml += f'\n\t\t\t\n\t\t\t\t{CLASS_IDS[i]}\n\t\t\t\t{co_ids[i]}\n\t\t\t' child_xml = '\n\t\t\t\u0420\u0443\u0441\u0441\u043a\u0438\u0439' # Русский + for cm_name in common_modules: + child_xml += '\n\t\t\t%s' % cm_name for meta_type, names in type_map.items(): if meta_type not in META_INFO: continue @@ -1381,6 +1482,8 @@ def main(): """ write_bom(os.path.join(lang_dir, '\u0420\u0443\u0441\u0441\u043a\u0438\u0439.xml'), lang_xml) + write_common_module_stubs(cfg_dir, ns_decl, common_modules) + # Metadata stubs for meta_type, names in type_map.items(): if meta_type not in META_INFO: diff --git a/.claude/skills/erf-build/SKILL.md b/.claude/skills/erf-build/SKILL.md index 066f90b40..c2579748c 100644 --- a/.claude/skills/erf-build/SKILL.md +++ b/.claude/skills/erf-build/SKILL.md @@ -60,6 +60,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu | `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | | `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` | | `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` | +| `-ConfigSrc <путь>` | нет | Каталог XML-выгрузки конфигурации, в которой будет работать отчёт. По умолчанию — `configSrc` указанной базы из `.v8-project.json` | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` | @@ -74,6 +75,12 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает. +Общие модули конфигурации (`ОбщегоНазначения`, `УправлениеПечатью` и т.п.) проверка видит только +при известной выгрузке конфигурации — `-ConfigSrc` или `configSrc` базы в `.v8-project.json`. +Без неё обращения к ним — ошибка «Переменная не определена»; тогда укажи выгрузку или собери +с `-Checks off`. Методы глобальных общих модулей, вызванные без имени модуля, проверка не видит +и с выгрузкой — для такого кода остаётся `-Checks off`. + ## Примеры ```powershell diff --git a/tests/skills/cases/epf-build/check-hint-configsrc.json b/tests/skills/cases/epf-build/check-hint-configsrc.json new file mode 100644 index 000000000..7d7a1afae --- /dev/null +++ b/tests/skills/cases/epf-build/check-hint-configsrc.json @@ -0,0 +1,58 @@ +{ + "name": "С -ConfigSrc: модуль есть, но не в контексте / модуля нет — разные подсказки, без -Checks off (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "", + "check": { + "exit": 101, + "log": "{Обработка.Тест.МодульОбъекта(2,2)}: Переменная не определена (ОбщегоНазначения)\r\n\t<>ОбщегоНазначения.СообщитьПользователю(\"x\"); (Проверка: Сервер)\r\n{Обработка.Тест.МодульОбъекта(3,2)}: Переменная не определена (ОбщегоНазначениеее)\r\n\t<>ОбщегоНазначениеее.Метод(); (Проверка: Сервер)\r\n" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + }, + { + "writeFile": { + "path": "src/Тест/Ext/ObjectModule.bsl", + "content": "Процедура Проба()\r\n\tОбщегоНазначения.СообщитьПользователю(\"x\");\r\nКонецПроцедуры\r\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/ОбщегоНазначения.xml", + "content": "\n\n\t\n\t\t\n\t\t\tОбщегоНазначения\n\t\t\ttrue\n\t\t\n\t\n\n" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf", + "-ConfigSrc", + "{workDir}\\cfgsrc" + ], + "expect": { + "stdoutContains": [ + "Общие модули из configSrc: ОбщегоНазначения", + "[hint] общий модуль ОбщегоНазначения есть в configSrc, но недоступен в контексте проверки", + "[hint] общего модуля ОбщегоНазначениеее нет в configSrc" + ], + "stdoutNotContains": [ + "/LoadExternalDataProcessorOrReportFromFiles", + "-Checks off" + ] + }, + "expectError": true, + "noSnapshot": "сборка отменена — артефакта нет по замыслу" +} \ No newline at end of file diff --git a/tests/skills/cases/epf-build/check-hint-no-configsrc.json b/tests/skills/cases/epf-build/check-hint-no-configsrc.json new file mode 100644 index 000000000..76c196230 --- /dev/null +++ b/tests/skills/cases/epf-build/check-hint-no-configsrc.json @@ -0,0 +1,44 @@ +{ + "name": "Неопределённый общий модуль без configSrc: подсказка про -ConfigSrc и -Checks off (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "", + "check": { + "exit": 101, + "log": "{Обработка.Тест.МодульОбъекта(2,2)}: Переменная не определена (ОбщегоНазначения)\r\n\t<>ОбщегоНазначения.СообщитьПользователю(\"x\"); (Проверка: Сервер)\r\n{Обработка.Тест.МодульОбъекта(3,2)}: Переменная не определена (ОбщегоНазначения)\r\n\t<>ОбщегоНазначения.Прочее(); (Проверка: Сервер)\r\n{Обработка.Тест.МодульОбъекта(4,2)}: Переменная не определена (Сумма)\r\n\tХ = <>Сумма + 1; (Проверка: Сервер)\r\n" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "[hint] похоже на общий модуль конфигурации", + "[hint] Общие модули конфигурации проверке не видны: ОбщегоНазначения.", + "-Checks off" + ], + "stdoutNotContains": [ + "/LoadExternalDataProcessorOrReportFromFiles", + "проверке не видны: ОбщегоНазначения, Сумма" + ] + }, + "expectError": true, + "noSnapshot": "сборка отменена — артефакта нет по замыслу" +} \ No newline at end of file diff --git a/tests/skills/cases/stub-db-create/configsrc-common-modules.json b/tests/skills/cases/stub-db-create/configsrc-common-modules.json new file mode 100644 index 000000000..cb5d61cbf --- /dev/null +++ b/tests/skills/cases/stub-db-create/configsrc-common-modules.json @@ -0,0 +1,129 @@ +{ + "name": "Общие модули из -ConfigSrc: только те, к которым обращается код, с флагами из выгрузки", + "fakePlatform": { + "exit": 1, + "log": "" + }, + "preRun": [ + { + "writeFile": { + "path": "src/Проба.xml", + "content": "\n\n\t\n\t\t\n\t\t\t\n\t\t\t\tcccccccc-4444-5555-6666-dddddddddddd\n\t\t\t\teeeeeeee-7777-8888-9999-ffffffffffff\n\t\t\t\n\t\t\n\t\t\n\t\t\tПроба\n\t\t\n\t\t\n\t\n\n" + } + }, + { + "writeFile": { + "path": "src/Проба/Ext/ObjectModule.bsl", + "content": "Процедура Проба()\r\n\tОбщМод.Метод();\r\n\tобщмод2.Метод(\"ЛишнийМодуль.Х\"); // КомментМодуль.Х\r\n\tТекст = \"ВЫБРАТЬ\r\n\t|ТекстМодуль.Поле\";\r\n\tНетВКонфиге.Метод();\r\n\tЗапрос.Текст = Модуль3 .Поле;\r\nТ = \"ВЫБРАТЬ\r\n\t|ГДЕ Ложь\"; Модуль4.Метод();\r\nКонецПроцедуры\r\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/ОбщМод.xml", + "content": "\n\n\t\n\t\t\n\t\t\tОбщМод\n\t\t\tfalse\n\t\t\tfalse\n\t\t\ttrue\n\t\t\ttrue\n\t\t\tfalse\n\t\t\ttrue\n\t\t\tfalse\n\t\t\tDuringRequest\n\t\t\n\t\n\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/ОбщМод2.xml", + "content": "\n\n\t\n\t\t\n\t\t\tОбщМод2\n\t\t\ttrue\n\t\t\ttrue\n\t\t\n\t\n\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/Модуль3.xml", + "content": "\n\n\t\n\t\t\n\t\t\tМодуль3\n\t\t\ttrue\n\t\t\n\t\n\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/ЛишнийМодуль.xml", + "content": "\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/КомментМодуль.xml", + "content": "\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/ТекстМодуль.xml", + "content": "\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/НеИспользуется.xml", + "content": "\n" + } + }, + { + "writeFile": { + "path": "cfgsrc/CommonModules/Модуль4.xml", + "content": "\n\n\t\n\t\t\n\t\t\tМодуль4\n\t\t\ttrue\n\t\t\n\t\n\n" + } + } + ], + "args_extra": [ + "-SourceDir", + "{workDir}/src", + "-V8Path", + "{fakePlatform}", + "-TempBasePath", + "{workDir}/base", + "-EmbedSourceFile", + "{workDir}/src/Проба.xml", + "-ConfigSrc", + "{workDir}/cfgsrc" + ], + "expect": { + "stdoutContains": [ + "Общие модули из configSrc: Модуль3, Модуль4, ОбщМод, ОбщМод2" + ], + "files": [ + "base/cfg/CommonModules/ОбщМод/Ext/Module.bsl", + "base/cfg/CommonModules/ОбщМод2/Ext/Module.bsl", + "base/cfg/CommonModules/Модуль3.xml" + ], + "filesAbsent": [ + "base/cfg/CommonModules/ЛишнийМодуль.xml", + "base/cfg/CommonModules/КомментМодуль.xml", + "base/cfg/CommonModules/ТекстМодуль.xml", + "base/cfg/CommonModules/НеИспользуется.xml", + "base/cfg/CommonModules/НетВКонфиге.xml" + ], + "fileContains": [ + { + "file": "base/cfg/CommonModules/ОбщМод.xml", + "text": [ + "ОбщМод", + "false", + "true", + "true", + "true", + "version=\"2.17\"" + ] + }, + { + "file": "base/cfg/CommonModules/ОбщМод2.xml", + "text": [ + "ОбщМод2", + "true", + "false" + ] + }, + { + "file": "base/cfg/Configuration.xml", + "text": [ + "ОбщМод", + "ОбщМод2", + "Модуль3" + ] + } + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая: проверяется сгенерированная конфигурация, а не база" +} \ No newline at end of file