@abapify/adt-atc 0.4.46 → 0.4.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"abapgit-D-WCS83I.mjs","names":["resolvePath"],"sources":["../src/resolvers/adt-uri-to-abapgit-path.ts","../src/resolvers/abapgit.ts"],"sourcesContent":["/**\n * Convert an ADT URI to an on-disk abapGit filename.\n *\n * This mapper belongs next to the ATC resolver because ATC findings carry\n * ADT locations while the GitLab report needs an abapGit file name. The\n * abapGit format plugin re-exports it for consumers that already use that\n * package.\n */\n\n/** Apply abapGit namespace transform: `/MYNS/zcl_foo` → `(myns)zcl_foo`. */\nfunction applyNamespace(raw: string): string {\n const nsMatch = /^\\/([^/]+)\\/(.*)$/.exec(raw);\n if (nsMatch) {\n const [, ns, rest] = nsMatch;\n return `(${ns.toLowerCase()})${rest.toLowerCase()}`;\n }\n return raw.toLowerCase();\n}\n\nfunction stripFragment(uri: string): string {\n const hashIndex = uri.indexOf('#');\n return hashIndex >= 0 ? uri.slice(0, hashIndex) : uri;\n}\n\n// Class include → abapGit suffix. Kept here so the built-in ATC package does\n// not depend on the format plugin (which depends on @abapify/adt-atc).\n// `main` maps to the primary class source (no suffix).\nconst CLASS_INCLUDE_TO_SUFFIX: Record<string, string | undefined> = {\n testclasses: 'testclasses',\n definitions: 'locals_def',\n implementations: 'locals_imp',\n macros: 'macros',\n localtypes: 'locals_types',\n};\n\ninterface Rule {\n test: RegExp;\n build: (match: RegExpMatchArray) => string;\n}\n\n// Order matters – most specific first.\nconst RULES: Rule[] = [\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/includes\\/([^/]+)$/,\n build: (match) => {\n const name = applyNamespace(decodeURIComponent(match[1]));\n const include = match[2].toLowerCase();\n if (include === 'main') return `src/${name}.clas.abap`;\n const suffix = CLASS_INCLUDE_TO_SUFFIX[include];\n return suffix\n ? `src/${name}.clas.${suffix}.abap`\n : `src/${name}.clas.${include}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/source\\/main$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n\n // Interfaces\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/interfaces\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.intf.abap`,\n },\n\n // Programs\n {\n test: /^\\/sap\\/bc\\/adt\\/programs\\/programs\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.prog.abap`,\n },\n\n // Function group metadata and source components\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/source\\/main$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n return `src/${group}.fugr.l${group}top.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.fugr.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/fmodules\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const functionModule = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${functionModule}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/includes\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const include = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${include}.abap`;\n },\n },\n\n // CDS and RAP source objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/ddl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.ddls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/acm\\/dcl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dcls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/bo\\/behaviordefinitions\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.bdef.abdl`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/srvd\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvd.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/businessservices\\/bindings\\/([^/]+?)(?:\\/publishedstates)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvb.xml`,\n },\n\n // DDIC objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/domains\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.doma.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/dataelements\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dtel.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/(?:structures|tables)\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.tabl.xml`,\n },\n\n // Package — abapGit serializes every package to the fixed filename\n // `package.devc.xml`, regardless of the package name.\n {\n test: /^\\/sap\\/bc\\/adt\\/packages\\/([^/]+)$/,\n build: () => 'src/package.devc.xml',\n },\n];\n\n/**\n * Convert an ADT URI to the abapGit on-disk filename. The returned path is\n * rooted at the configured abapGit starting folder (`src/` by default);\n * callers resolve that prefix against the repository's actual source tree.\n */\nexport function adtUriToAbapGitPath(uri: string): string | null {\n if (!uri) return null;\n const clean = stripFragment(uri).trim();\n for (const rule of RULES) {\n const match = rule.test.exec(clean);\n if (match) {\n try {\n return rule.build(match);\n } catch (error) {\n if (error instanceof URIError) return null;\n throw error;\n }\n }\n }\n return null;\n}\n","/**\n * Built-in abapGit Finding Resolver.\n *\n * ATC identifies a finding by an ADT URI, while GitLab needs the path of the\n * serialized abapGit file. The resolver owns that format-specific mapping and\n * scans the checked-out repository so PREFIX and FULL layouts both resolve to\n * the path that actually exists in Git.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n realpathSync,\n type Dirent,\n} from 'node:fs';\nimport {\n basename,\n join,\n relative,\n resolve as resolvePath,\n sep,\n} from 'node:path';\nimport type { FindingResolver, ResolvedLocation } from '../types';\nimport { adtUriToAbapGitPath } from './adt-uri-to-abapgit-path';\n\ntype FolderLogic = 'prefix' | 'full' | 'full-with-root';\n\ninterface AbapGitMetadata {\n folderLogic?: FolderLogic;\n startingFolder: string;\n}\n\ninterface ResolverRepository {\n root: string;\n sourceRoot: string;\n metadata?: AbapGitMetadata;\n configuredFormat?: string;\n configuredFolderLogic?: FolderLogic;\n}\n\nfunction toPosix(pathValue: string): string {\n return sep === '/' ? pathValue : pathValue.split(sep).join('/');\n}\n\nfunction normalizeStartingFolder(value: string | undefined): string {\n const normalized = (value || 'src')\n .trim()\n .replaceAll('\\\\', '/')\n .replace(/^\\/+/, '')\n .replace(/(?<!\\/)\\/+$/, '');\n return normalized || 'src';\n}\n\nfunction parseFolderLogic(value: string | undefined): FolderLogic | undefined {\n const normalized = value?.trim().toLowerCase();\n if (\n normalized === 'prefix' ||\n normalized === 'full' ||\n normalized === 'full-with-root'\n ) {\n return normalized;\n }\n return undefined;\n}\n\nfunction parseAbapGitMetadata(root: string): AbapGitMetadata | undefined {\n const metadataPath = join(root, '.abapgit.xml');\n if (!existsSync(metadataPath)) return undefined;\n\n try {\n const xml = readFileSync(metadataPath, 'utf8');\n return {\n folderLogic: parseFolderLogic(\n xml.match(/<FOLDER_LOGIC>([^<]*)<\\/FOLDER_LOGIC>/i)?.[1],\n ),\n startingFolder: normalizeStartingFolder(\n xml.match(/<STARTING_FOLDER>([^<]*)<\\/STARTING_FOLDER>/i)?.[1],\n ),\n };\n } catch {\n return undefined;\n }\n}\n\nfunction parseConfiguredFormat(root: string): {\n format?: string;\n folderLogic?: FolderLogic;\n} {\n const configPaths = [\n process.env.ADT_CONFIG_PATH,\n join(root, 'adt.config.ts'),\n join(root, 'adt.config.js'),\n join(root, 'adt.config.mjs'),\n ].filter((value): value is string => Boolean(value));\n\n const resolvedConfigPaths = new Set<string>();\n for (const value of configPaths) {\n if (/\\.\\./.test(value)) continue;\n resolvedConfigPaths.add(resolvePath(value));\n }\n\n for (const configPath of resolvedConfigPaths) {\n if (!existsSync(configPath)) continue;\n try {\n const config = readFileSync(configPath, 'utf8');\n const formatId = config.match(\n /\\bformat\\s*:\\s*\\{[\\s\\S]*?\\bid\\s*:\\s*['\"]([^'\"]+)['\"]/i,\n )?.[1];\n const folderLogic = parseFolderLogic(\n config.match(/\\bfolderLogic\\s*:\\s*['\"]([^'\"]+)['\"]/i)?.[1],\n );\n if (formatId || folderLogic) {\n return {\n ...(formatId ? { format: formatId.trim().toLowerCase() } : {}),\n ...(folderLogic ? { folderLogic } : {}),\n };\n }\n } catch {\n // An unreadable optional config must not disable report generation.\n }\n }\n return {};\n}\n\nfunction findRepositoryRoot(): string {\n const configuredRoot =\n process.env.CI_PROJECT_DIR?.trim() || process.env.SOURCE_REPO_DIR?.trim();\n if (configuredRoot) {\n if (/\\.\\./.test(configuredRoot)) {\n throw new Error(\n 'Repository root must not contain path traversal segments',\n );\n }\n return resolvePath(configuredRoot);\n }\n\n // Preserve the old API behaviour for callers that pass an explicit source\n // path: paths are still reported relative to the current working directory.\n return process.cwd();\n}\n\nfunction resolveRepository(srcRoot: string): ResolverRepository {\n const usesConfiguredSourceRoot = srcRoot === 'src/' || srcRoot === 'src';\n const root = usesConfiguredSourceRoot ? findRepositoryRoot() : process.cwd();\n const metadata = parseAbapGitMetadata(root);\n const configured = parseConfiguredFormat(root);\n const configuredSourceFolder = metadata?.startingFolder ?? 'src';\n const sourcePath = usesConfiguredSourceRoot\n ? join(root, configuredSourceFolder)\n : resolvePath(process.cwd(), srcRoot);\n\n // Guard against path traversal via STARTING_FOLDER — the resolved source\n // root must stay inside the repository root. Use realpath to resolve\n // symlinks before the containment check. If it escapes, fall back to\n // the default `src` folder. Guard existsSync before realpathSync so a\n // missing source folder does not throw and abort report generation.\n const realRoot = existsSync(root) ? realpathSync(root) : root;\n const realSourcePath = existsSync(sourcePath)\n ? realpathSync(sourcePath)\n : sourcePath;\n const safeSourcePath =\n realSourcePath === realRoot || realSourcePath.startsWith(realRoot + sep)\n ? sourcePath\n : join(root, 'src');\n\n return {\n root,\n sourceRoot: safeSourcePath,\n metadata,\n configuredFormat: configured.format,\n configuredFolderLogic: configured.folderLogic,\n };\n}\n\n// ── Source tree and file contents ───────────────────────────────────────\n\nconst fileCache = new Map<string, string[]>();\n\nconst SOURCE_EXTENSIONS = ['.abap', '.xml', '.acds', '.abdl'];\n\nfunction hasSourceExtension(filename: string): boolean {\n return SOURCE_EXTENSIONS.some((ext) => filename.endsWith(ext));\n}\n\nfunction indexEntry(\n entry: Dirent,\n current: string,\n result: string[],\n stack: string[],\n): void {\n const fullPath = join(current, entry.name);\n if (entry.isDirectory()) {\n stack.push(fullPath);\n } else if (entry.isFile() && hasSourceExtension(entry.name)) {\n result.push(fullPath);\n }\n}\n\nfunction collectSourceFiles(root: string): string[] {\n const result: string[] = [];\n const stack: string[] = [root];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n let entries: Dirent[];\n try {\n entries = readdirSync(current, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n indexEntry(entry, current, result, stack);\n }\n }\n\n return result;\n}\n\nfunction getFileLines(filePath: string): string[] | null {\n const cached = fileCache.get(filePath);\n if (cached) return cached;\n try {\n const lines = readFileSync(filePath, 'utf8').split('\\n');\n fileCache.set(filePath, lines);\n return lines;\n } catch {\n return null;\n }\n}\n\n// ── Method range parsing ────────────────────────────────────────────────\n\ninterface MethodRange {\n name: string;\n startLine: number;\n length: number;\n}\n\nfunction parseMethodRanges(lines: string[]): MethodRange[] {\n const ranges: MethodRange[] = [];\n let currentMethod: string | null = null;\n let methodStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const match = /^\\s*METHOD\\s+(\\w+)/i.exec(lines[i]);\n if (match) {\n currentMethod = match[1].toLowerCase();\n methodStart = i + 1;\n }\n if (currentMethod && /^\\s*ENDMETHOD/i.test(lines[i])) {\n const endLine = i + 1;\n ranges.push({\n name: currentMethod,\n startLine: methodStart,\n length: endLine - methodStart + 1,\n });\n currentMethod = null;\n }\n }\n return ranges;\n}\n\nfunction convertLine(\n atcLine: number,\n methodName: string | undefined,\n filePath: string,\n): number {\n if (!filePath.endsWith('.clas.abap')) return atcLine;\n\n const lines = getFileLines(filePath);\n if (!lines) return atcLine;\n\n const ranges = parseMethodRanges(lines);\n if (ranges.length === 0) return atcLine;\n\n if (methodName) {\n const method = ranges.find(\n (range) => range.name === methodName.toLowerCase(),\n );\n if (method) return method.startLine + atcLine - 1;\n }\n\n // Without a method name the ATC line is already relative to the class\n // file, so return it as-is instead of guessing a method.\n return atcLine;\n}\n\n// ── Resolver factory ────────────────────────────────────────────────────\n\n/**\n * Create a built-in abapGit finding resolver.\n *\n * The full ATC location is used to derive the canonical abapGit basename.\n * The actual checked-out path is then selected from the configured source\n * tree, which naturally preserves PREFIX/FULL package directories and the\n * repository's STARTING_FOLDER.\n */\nexport function createAbapGitResolver(srcRoot = 'src/'): FindingResolver {\n const repository = resolveRepository(srcRoot);\n const lookup = new Map<string, string>();\n\n try {\n if (existsSync(repository.sourceRoot)) {\n const files = collectSourceFiles(repository.sourceRoot).sort(\n (left, right) => left.localeCompare(right),\n );\n\n const ambiguous = new Set<string>();\n for (const filePath of files) {\n const name = basename(filePath).toLowerCase();\n if (lookup.has(name)) {\n ambiguous.add(name);\n } else {\n lookup.set(name, filePath);\n }\n }\n for (const name of ambiguous) lookup.delete(name);\n }\n } catch {\n // Source scan failed — resolver will return null for all findings.\n }\n\n if (lookup.size > 0) {\n const metadata = repository.metadata;\n const format = repository.configuredFormat ?? 'abapgit';\n const folderLogic =\n metadata?.folderLogic ?? repository.configuredFolderLogic ?? 'prefix';\n const startingFolder = metadata?.startingFolder ?? 'src';\n console.log(\n `📂 Finding resolver: ${lookup.size} files indexed (format=${format}, folderLogic=${folderLogic}, startingFolder=${startingFolder})`,\n );\n }\n\n return {\n resolve(\n objectType: string,\n objectName: string,\n atcLine: number,\n methodName?: string,\n atcLocation?: string,\n ): Promise<ResolvedLocation | null> {\n const canonicalPath = atcLocation\n ? adtUriToAbapGitPath(atcLocation)\n : null;\n const candidatePath =\n canonicalPath ??\n `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`;\n const candidateSegments = candidatePath.split('/');\n const expectedFilename = candidateSegments.at(-1);\n if (!expectedFilename) return Promise.resolve(null);\n const resolvedPath = lookup.get(expectedFilename.toLowerCase());\n\n if (!resolvedPath) return Promise.resolve(null);\n\n const fileLine = convertLine(atcLine, methodName, resolvedPath);\n const gitPath = toPosix(relative(repository.root, resolvedPath));\n if (!gitPath || gitPath.startsWith('../')) return Promise.resolve(null);\n\n return Promise.resolve({ path: gitPath, line: fileLine });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"abapgit-D-WCS83I.mjs","names":["resolvePath"],"sources":["../src/resolvers/adt-uri-to-abapgit-path.ts","../src/resolvers/abapgit.ts"],"sourcesContent":["/**\n * Convert an ADT URI to an on-disk abapGit filename.\n *\n * This mapper belongs next to the ATC resolver because ATC findings carry\n * ADT locations while the GitLab report needs an abapGit file name. The\n * abapGit format plugin re-exports it for consumers that already use that\n * package.\n */\n\n/** Apply abapGit namespace transform: `/MYNS/zcl_foo` → `(myns)zcl_foo`. */\nfunction applyNamespace(raw: string): string {\n const nsMatch = /^\\/([^/]+)\\/(.*)$/.exec(raw);\n if (nsMatch) {\n const [, ns, rest] = nsMatch;\n return `(${ns.toLowerCase()})${rest.toLowerCase()}`;\n }\n return raw.toLowerCase();\n}\n\nfunction stripFragment(uri: string): string {\n const hashIndex = uri.indexOf('#');\n return hashIndex >= 0 ? uri.slice(0, hashIndex) : uri;\n}\n\n// Class include → abapGit suffix. Kept here so the built-in ATC package does\n// not depend on the format plugin (which depends on @abapify/adt-atc).\n// `main` maps to the primary class source (no suffix).\nconst CLASS_INCLUDE_TO_SUFFIX: Record<string, string | undefined> = {\n testclasses: 'testclasses',\n definitions: 'locals_def',\n implementations: 'locals_imp',\n macros: 'macros',\n localtypes: 'locals_types',\n};\n\ninterface Rule {\n test: RegExp;\n build: (match: RegExpMatchArray) => string;\n}\n\n// Order matters – most specific first.\nconst RULES: Rule[] = [\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/includes\\/([^/]+)$/,\n build: (match) => {\n const name = applyNamespace(decodeURIComponent(match[1]));\n const include = match[2].toLowerCase();\n if (include === 'main') return `src/${name}.clas.abap`;\n const suffix = CLASS_INCLUDE_TO_SUFFIX[include];\n return suffix\n ? `src/${name}.clas.${suffix}.abap`\n : `src/${name}.clas.${include}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)\\/source\\/main$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/classes\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.clas.abap`,\n },\n\n // Interfaces\n {\n test: /^\\/sap\\/bc\\/adt\\/oo\\/interfaces\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.intf.abap`,\n },\n\n // Programs\n {\n test: /^\\/sap\\/bc\\/adt\\/programs\\/programs\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.prog.abap`,\n },\n\n // Function group metadata and source components\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/source\\/main$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n return `src/${group}.fugr.l${group}top.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.fugr.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/fmodules\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const functionModule = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${functionModule}.abap`;\n },\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/functions\\/groups\\/([^/]+)\\/includes\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) => {\n const group = applyNamespace(decodeURIComponent(match[1]));\n const include = applyNamespace(decodeURIComponent(match[2]));\n return `src/${group}.fugr.${include}.abap`;\n },\n },\n\n // CDS and RAP source objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/ddl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.ddls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/acm\\/dcl\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dcls.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/bo\\/behaviordefinitions\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.bdef.abdl`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/srvd\\/sources\\/([^/]+?)(?:\\/source\\/main)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvd.acds`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/businessservices\\/bindings\\/([^/]+?)(?:\\/publishedstates)?$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.srvb.xml`,\n },\n\n // DDIC objects\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/domains\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.doma.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/dataelements\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.dtel.xml`,\n },\n {\n test: /^\\/sap\\/bc\\/adt\\/ddic\\/(?:structures|tables)\\/([^/]+)$/,\n build: (match) =>\n `src/${applyNamespace(decodeURIComponent(match[1]))}.tabl.xml`,\n },\n\n // Package — abapGit serializes every package to the fixed filename\n // `package.devc.xml`, regardless of the package name.\n {\n test: /^\\/sap\\/bc\\/adt\\/packages\\/([^/]+)$/,\n build: () => 'src/package.devc.xml',\n },\n];\n\n/**\n * Convert an ADT URI to the abapGit on-disk filename. The returned path is\n * rooted at the configured abapGit starting folder (`src/` by default);\n * callers resolve that prefix against the repository's actual source tree.\n */\nexport function adtUriToAbapGitPath(uri: string): string | null {\n if (!uri) return null;\n const clean = stripFragment(uri).trim();\n for (const rule of RULES) {\n const match = rule.test.exec(clean);\n if (match) {\n try {\n return rule.build(match);\n } catch (error) {\n if (error instanceof URIError) return null;\n throw error;\n }\n }\n }\n return null;\n}\n","/**\n * Built-in abapGit Finding Resolver.\n *\n * ATC identifies a finding by an ADT URI, while GitLab needs the path of the\n * serialized abapGit file. The resolver owns that format-specific mapping and\n * scans the checked-out repository so PREFIX and FULL layouts both resolve to\n * the path that actually exists in Git.\n */\n\nimport {\n existsSync,\n readFileSync,\n readdirSync,\n realpathSync,\n type Dirent,\n} from 'node:fs';\nimport {\n basename,\n join,\n relative,\n resolve as resolvePath,\n sep,\n} from 'node:path';\nimport type { FindingResolver, ResolvedLocation } from '../types';\nimport { adtUriToAbapGitPath } from './adt-uri-to-abapgit-path';\n\ntype FolderLogic = 'prefix' | 'full' | 'full-with-root';\n\ninterface AbapGitMetadata {\n folderLogic?: FolderLogic;\n startingFolder: string;\n}\n\ninterface ResolverRepository {\n root: string;\n sourceRoot: string;\n metadata?: AbapGitMetadata;\n configuredFormat?: string;\n configuredFolderLogic?: FolderLogic;\n}\n\nfunction toPosix(pathValue: string): string {\n return sep === '/' ? pathValue : pathValue.split(sep).join('/');\n}\n\nfunction normalizeStartingFolder(value: string | undefined): string {\n const normalized = (value || 'src')\n .trim()\n .replaceAll('\\\\', '/')\n .replace(/^\\/+/, '')\n .replace(/(?<!\\/)\\/+$/, '');\n return normalized || 'src';\n}\n\nfunction parseFolderLogic(value: string | undefined): FolderLogic | undefined {\n const normalized = value?.trim().toLowerCase();\n if (\n normalized === 'prefix' ||\n normalized === 'full' ||\n normalized === 'full-with-root'\n ) {\n return normalized;\n }\n return undefined;\n}\n\nfunction parseAbapGitMetadata(root: string): AbapGitMetadata | undefined {\n const metadataPath = join(root, '.abapgit.xml');\n if (!existsSync(metadataPath)) return undefined;\n\n try {\n const xml = readFileSync(metadataPath, 'utf8');\n return {\n folderLogic: parseFolderLogic(\n xml.match(/<FOLDER_LOGIC>([^<]*)<\\/FOLDER_LOGIC>/i)?.[1],\n ),\n startingFolder: normalizeStartingFolder(\n xml.match(/<STARTING_FOLDER>([^<]*)<\\/STARTING_FOLDER>/i)?.[1],\n ),\n };\n } catch {\n return undefined;\n }\n}\n\nfunction parseConfiguredFormat(root: string): {\n format?: string;\n folderLogic?: FolderLogic;\n} {\n const configPaths = [\n process.env.ADT_CONFIG_PATH,\n join(root, 'adt.config.ts'),\n join(root, 'adt.config.js'),\n join(root, 'adt.config.mjs'),\n ].filter((value): value is string => Boolean(value));\n\n const resolvedConfigPaths = new Set<string>();\n for (const value of configPaths) {\n if (/\\.\\./.test(value)) continue;\n resolvedConfigPaths.add(resolvePath(value));\n }\n\n for (const configPath of resolvedConfigPaths) {\n if (!existsSync(configPath)) continue; // lgtm[js/path-injection]\n try {\n const config = readFileSync(configPath, 'utf8'); // lgtm[js/path-injection]\n const formatId = config.match(\n /\\bformat\\s*:\\s*\\{[\\s\\S]*?\\bid\\s*:\\s*['\"]([^'\"]+)['\"]/i,\n )?.[1];\n const folderLogic = parseFolderLogic(\n config.match(/\\bfolderLogic\\s*:\\s*['\"]([^'\"]+)['\"]/i)?.[1],\n );\n if (formatId || folderLogic) {\n return {\n ...(formatId ? { format: formatId.trim().toLowerCase() } : {}),\n ...(folderLogic ? { folderLogic } : {}),\n };\n }\n } catch {\n // An unreadable optional config must not disable report generation.\n }\n }\n return {};\n}\n\nfunction findRepositoryRoot(): string {\n const configuredRoot =\n process.env.CI_PROJECT_DIR?.trim() || process.env.SOURCE_REPO_DIR?.trim();\n if (configuredRoot) {\n if (/\\.\\./.test(configuredRoot)) {\n throw new Error(\n 'Repository root must not contain path traversal segments',\n );\n }\n return resolvePath(configuredRoot);\n }\n\n // Preserve the old API behaviour for callers that pass an explicit source\n // path: paths are still reported relative to the current working directory.\n return process.cwd();\n}\n\nfunction resolveRepository(srcRoot: string): ResolverRepository {\n const usesConfiguredSourceRoot = srcRoot === 'src/' || srcRoot === 'src';\n const root = usesConfiguredSourceRoot ? findRepositoryRoot() : process.cwd();\n const metadata = parseAbapGitMetadata(root);\n const configured = parseConfiguredFormat(root);\n const configuredSourceFolder = metadata?.startingFolder ?? 'src';\n const sourcePath = usesConfiguredSourceRoot\n ? join(root, configuredSourceFolder)\n : resolvePath(process.cwd(), srcRoot);\n\n // Guard against path traversal via STARTING_FOLDER — the resolved source\n // root must stay inside the repository root. Use realpath to resolve\n // symlinks before the containment check. If it escapes, fall back to\n // the default `src` folder. Guard existsSync before realpathSync so a\n // missing source folder does not throw and abort report generation.\n const realRoot = existsSync(root) ? realpathSync(root) : root; // lgtm[js/path-injection]\n const realSourcePath = existsSync(sourcePath) // lgtm[js/path-injection]\n ? realpathSync(sourcePath)\n : sourcePath;\n const safeSourcePath =\n realSourcePath === realRoot || realSourcePath.startsWith(realRoot + sep)\n ? sourcePath\n : join(root, 'src');\n\n return {\n root,\n sourceRoot: safeSourcePath,\n metadata,\n configuredFormat: configured.format,\n configuredFolderLogic: configured.folderLogic,\n };\n}\n\n// ── Source tree and file contents ───────────────────────────────────────\n\nconst fileCache = new Map<string, string[]>();\n\nconst SOURCE_EXTENSIONS = ['.abap', '.xml', '.acds', '.abdl'];\n\nfunction hasSourceExtension(filename: string): boolean {\n return SOURCE_EXTENSIONS.some((ext) => filename.endsWith(ext));\n}\n\nfunction indexEntry(\n entry: Dirent,\n current: string,\n result: string[],\n stack: string[],\n): void {\n const fullPath = join(current, entry.name);\n if (entry.isDirectory()) {\n stack.push(fullPath);\n } else if (entry.isFile() && hasSourceExtension(entry.name)) {\n result.push(fullPath);\n }\n}\n\nfunction collectSourceFiles(root: string): string[] {\n const result: string[] = [];\n const stack: string[] = [root];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n let entries: Dirent[];\n try {\n entries = readdirSync(current, { withFileTypes: true }); // lgtm[js/path-injection]\n } catch {\n continue;\n }\n for (const entry of entries) {\n indexEntry(entry, current, result, stack);\n }\n }\n\n return result;\n}\n\nfunction getFileLines(filePath: string): string[] | null {\n const cached = fileCache.get(filePath);\n if (cached) return cached;\n try {\n const lines = readFileSync(filePath, 'utf8').split('\\n'); // lgtm[js/path-injection]\n fileCache.set(filePath, lines);\n return lines;\n } catch {\n return null;\n }\n}\n\n// ── Method range parsing ────────────────────────────────────────────────\n\ninterface MethodRange {\n name: string;\n startLine: number;\n length: number;\n}\n\nfunction parseMethodRanges(lines: string[]): MethodRange[] {\n const ranges: MethodRange[] = [];\n let currentMethod: string | null = null;\n let methodStart = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const match = /^\\s*METHOD\\s+(\\w+)/i.exec(lines[i]);\n if (match) {\n currentMethod = match[1].toLowerCase();\n methodStart = i + 1;\n }\n if (currentMethod && /^\\s*ENDMETHOD/i.test(lines[i])) {\n const endLine = i + 1;\n ranges.push({\n name: currentMethod,\n startLine: methodStart,\n length: endLine - methodStart + 1,\n });\n currentMethod = null;\n }\n }\n return ranges;\n}\n\nfunction convertLine(\n atcLine: number,\n methodName: string | undefined,\n filePath: string,\n): number {\n if (!filePath.endsWith('.clas.abap')) return atcLine;\n\n const lines = getFileLines(filePath);\n if (!lines) return atcLine;\n\n const ranges = parseMethodRanges(lines);\n if (ranges.length === 0) return atcLine;\n\n if (methodName) {\n const method = ranges.find(\n (range) => range.name === methodName.toLowerCase(),\n );\n if (method) return method.startLine + atcLine - 1;\n }\n\n // Without a method name the ATC line is already relative to the class\n // file, so return it as-is instead of guessing a method.\n return atcLine;\n}\n\n// ── Resolver factory ────────────────────────────────────────────────────\n\n/**\n * Create a built-in abapGit finding resolver.\n *\n * The full ATC location is used to derive the canonical abapGit basename.\n * The actual checked-out path is then selected from the configured source\n * tree, which naturally preserves PREFIX/FULL package directories and the\n * repository's STARTING_FOLDER.\n */\nexport function createAbapGitResolver(srcRoot = 'src/'): FindingResolver {\n const repository = resolveRepository(srcRoot);\n const lookup = new Map<string, string>();\n\n try {\n if (existsSync(repository.sourceRoot)) {\n // lgtm[js/path-injection]\n const files = collectSourceFiles(repository.sourceRoot).sort(\n (left, right) => left.localeCompare(right),\n );\n\n const ambiguous = new Set<string>();\n for (const filePath of files) {\n const name = basename(filePath).toLowerCase();\n if (lookup.has(name)) {\n ambiguous.add(name);\n } else {\n lookup.set(name, filePath);\n }\n }\n for (const name of ambiguous) lookup.delete(name);\n }\n } catch {\n // Source scan failed — resolver will return null for all findings.\n }\n\n if (lookup.size > 0) {\n const metadata = repository.metadata;\n const format = repository.configuredFormat ?? 'abapgit';\n const folderLogic =\n metadata?.folderLogic ?? repository.configuredFolderLogic ?? 'prefix';\n const startingFolder = metadata?.startingFolder ?? 'src';\n console.log(\n `📂 Finding resolver: ${lookup.size} files indexed (format=${format}, folderLogic=${folderLogic}, startingFolder=${startingFolder})`,\n );\n }\n\n return {\n resolve(\n objectType: string,\n objectName: string,\n atcLine: number,\n methodName?: string,\n atcLocation?: string,\n ): Promise<ResolvedLocation | null> {\n const canonicalPath = atcLocation\n ? adtUriToAbapGitPath(atcLocation)\n : null;\n const candidatePath =\n canonicalPath ??\n `${objectName.toLowerCase()}.${objectType.toLowerCase()}.abap`;\n const candidateSegments = candidatePath.split('/');\n const expectedFilename = candidateSegments.at(-1);\n if (!expectedFilename) return Promise.resolve(null);\n const resolvedPath = lookup.get(expectedFilename.toLowerCase());\n\n if (!resolvedPath) return Promise.resolve(null);\n\n const fileLine = convertLine(atcLine, methodName, resolvedPath);\n const gitPath = toPosix(relative(repository.root, resolvedPath));\n if (!gitPath || gitPath.startsWith('../')) return Promise.resolve(null);\n\n return Promise.resolve({ path: gitPath, line: fileLine });\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abapify/adt-atc",
3
- "version": "0.4.46",
3
+ "version": "0.4.47",
4
4
  "description": "ABAP Test Cockpit (ATC) CLI plugin for adt-cli",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,7 +28,7 @@
28
28
  "gitlab"
29
29
  ],
30
30
  "dependencies": {
31
- "@abapify/adt-plugin": "0.4.46",
31
+ "@abapify/adt-plugin": "0.4.47",
32
32
  "chalk": "^5.3.0"
33
33
  },
34
34
  "module": "./dist/index.mjs",