abap-mcp 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/abap/engine.d.ts +46 -0
- package/dist/abap/engine.js +143 -0
- package/dist/abap/formatter.d.ts +1 -0
- package/dist/abap/formatter.js +38 -0
- package/dist/abap/outline.d.ts +25 -0
- package/dist/abap/outline.js +70 -0
- package/dist/abap/readiness.d.ts +33 -0
- package/dist/abap/readiness.js +56 -0
- package/dist/abap/rules.d.ts +15 -0
- package/dist/abap/rules.js +51 -0
- package/dist/abap/scaffold.d.ts +55 -0
- package/dist/abap/scaffold.js +241 -0
- package/dist/abap.tools.d.ts +87 -0
- package/dist/abap.tools.js +435 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +16 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +29 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +11 -0
- package/dist/tool.d.ts +36 -0
- package/dist/tool.js +33 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akshay Palimkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# abap-mcp
|
|
2
|
+
|
|
3
|
+
**MCP server for SAP ABAP — offline static analysis, ABAP Cloud readiness, and RAP scaffolding.**
|
|
4
|
+
No SAP system. No credentials. Works on ABAP source wherever your AI agent works: a git checkout,
|
|
5
|
+
an abapGit export, a code review, CI.
|
|
6
|
+
|
|
7
|
+
Built on [abaplint](https://abaplint.org) (the open-source ABAP parser/linter) and the
|
|
8
|
+
[Model Context Protocol](https://modelcontextprotocol.io). TypeScript, 100% local — the server
|
|
9
|
+
makes **zero network calls** and touches **no filesystem**: sources go in as text, findings come
|
|
10
|
+
back as structured JSON.
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
Every other ABAP MCP server is either a **bridge to a live SAP system** (ADT/RFC — needs
|
|
15
|
+
credentials, a system, and trust) or a **documentation search**. But AI coding agents spend most
|
|
16
|
+
of their time where the *files* are — editing abapGit repos, reviewing diffs, generating code —
|
|
17
|
+
long before anything reaches a system. This server gives agents the missing feedback loop at that
|
|
18
|
+
layer:
|
|
19
|
+
|
|
20
|
+
- *"Does this ABAP parse? Is it clean?"* → `lint_abap`
|
|
21
|
+
- *"How far is this classic report from ABAP Cloud?"* → `check_cloud_readiness`
|
|
22
|
+
- *"Start me a correct RAP business object."* → `scaffold_rap_bo`
|
|
23
|
+
- *"What's in this 4,000-line class?"* → `get_abap_outline`
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Claude Code
|
|
29
|
+
claude mcp add abap-mcp -- npx -y abap-mcp
|
|
30
|
+
|
|
31
|
+
# or any MCP client (.mcp.json / mcp.json):
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"abap-mcp": { "command": "npx", "args": ["-y", "abap-mcp"] }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From a clone instead:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install && npm run build
|
|
43
|
+
claude mcp add abap-mcp -- node /path/to/abap-mcp/dist/cli.js
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then ask your agent things like *"lint this class against ABAP Cloud"*, *"is zold_report
|
|
47
|
+
cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, draft enabled"*.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
| Tool | What it does |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `lint_abap` | abaplint static analysis over ABAP/CDS/BDEF sources → structured findings with rule docs links. Presets: `style` (default, snippet-friendly), `full`, `syntax-only`; per-rule overrides. |
|
|
54
|
+
| `check_cloud_readiness` | Dual-parse diff (classic baseline vs `Cloud`): statements that are valid today but illegal in ABAP Cloud become categorized blockers (dynpro, list output, native SQL, …) with a transparent score; code broken at the baseline is reported separately, not counted as migration work. |
|
|
55
|
+
| `scaffold_rap_bo` | Generates the canonical RAP managed-BO stack (root view, behavior definition `strict(2)` + optional draft, behavior class + handler locals, projection, metadata extension, OData V4 service definition) plus suggested table DDL, activation order and next steps. |
|
|
56
|
+
| `list_abap_rules` | Browse abaplint's ~180 rules (filter by text or tag). |
|
|
57
|
+
| `explain_abap_rule` | One rule in depth — rationale (often Clean ABAP), examples, docs URL. |
|
|
58
|
+
| `format_abap` | Offline pretty-printer (keyword case + indentation). |
|
|
59
|
+
| `get_abap_outline` | Classes/methods/visibility/interfaces/FORMs of a source — navigate big objects without reading them whole. |
|
|
60
|
+
|
|
61
|
+
## Honesty box — what this is *not*
|
|
62
|
+
|
|
63
|
+
- **Not ATC.** Readiness here is *language-level*: statements ABAP Cloud removed. Whether your
|
|
64
|
+
code calls **released APIs only** requires a system's released-API list (ATC check
|
|
65
|
+
`SAP_CP_READINESS`) — out of scope for an offline tool, and the readiness report says so on
|
|
66
|
+
every call.
|
|
67
|
+
- **Scaffold validation is tiered.** Generated classes and CDS views are round-tripped through
|
|
68
|
+
abaplint at Cloud level before they're returned (the generator and the linter share one
|
|
69
|
+
parser). Behavior/service definitions are outside abaplint's checked surface — they are
|
|
70
|
+
golden-tested canonical templates, and ADT activation is the final arbiter. Each generated
|
|
71
|
+
file is labeled `validated: "abaplint" | "template"`.
|
|
72
|
+
- **Text-in only, by design.** No filesystem walking, no network — the entire attack surface is
|
|
73
|
+
a parser over strings you explicitly pass. For linting whole directories, use the
|
|
74
|
+
[abaplint CLI](https://abaplint.org) in CI, or the
|
|
75
|
+
[mcp-kit `wrap-abaplint` recipe](https://github.com/lumivarahq/mcp-kit) this server grew out of.
|
|
76
|
+
|
|
77
|
+
## Develop
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install
|
|
81
|
+
npm run check # typecheck + 80 tests + build — the CI gate
|
|
82
|
+
node dist/cli.js # stdio MCP server
|
|
83
|
+
npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Tool descriptions are CI-graded (a rubric test enforces verb-first names, when-to-use,
|
|
87
|
+
non-goals, described params, worked examples — the
|
|
88
|
+
[mcp-kit](https://github.com/lumivarahq/mcp-kit) discipline; the full mcp-kit lint scores all
|
|
89
|
+
seven tools 100/100).
|
|
90
|
+
|
|
91
|
+
## Design
|
|
92
|
+
|
|
93
|
+
The decision log — why offline, why abaplint, why a dual-parse readiness diff, why the
|
|
94
|
+
scaffolder validates its own output, what was deliberately left out — lives in
|
|
95
|
+
[`docs/DESIGN.md`](docs/DESIGN.md).
|
|
96
|
+
|
|
97
|
+
## Credits
|
|
98
|
+
|
|
99
|
+
- [abaplint](https://github.com/abaplint/abaplint) by Lars Hvam — the parser and rule engine
|
|
100
|
+
underneath every tool here (MIT).
|
|
101
|
+
- [mcp-kit](https://github.com/lumivarahq/mcp-kit) — the production-MCP patterns this server
|
|
102
|
+
follows (typed tool specs, transport discipline, description lint).
|
|
103
|
+
|
|
104
|
+
MIT © Akshay Palimkar. Not affiliated with or endorsed by SAP SE. "SAP", "ABAP" and "RAP" are
|
|
105
|
+
trademarks of SAP SE; this is an independent open-source tool for developers working with them.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface AbapSource {
|
|
2
|
+
filename?: string | undefined;
|
|
3
|
+
source: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Finding {
|
|
6
|
+
rule: string;
|
|
7
|
+
message: string;
|
|
8
|
+
severity: string;
|
|
9
|
+
file: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
/** First ~100 chars of the offending line, for fix-it-without-reopening flows. */
|
|
13
|
+
excerpt: string;
|
|
14
|
+
docsUrl: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const MAX_FILES = 32;
|
|
17
|
+
export declare const MAX_FILE_CHARS = 100000;
|
|
18
|
+
export declare const MAX_FINDINGS = 500;
|
|
19
|
+
/** ABAP language versions a caller may target. */
|
|
20
|
+
export declare const ABAP_VERSIONS: readonly ["Cloud", "v750", "v751", "v752", "v753", "v754", "v755", "v756", "v757", "v758"];
|
|
21
|
+
export type AbapVersion = (typeof ABAP_VERSIONS)[number];
|
|
22
|
+
/**
|
|
23
|
+
* Infer an abapGit-conventional filename from the source's first meaningful
|
|
24
|
+
* statement, so agents can lint snippets without knowing the convention.
|
|
25
|
+
*/
|
|
26
|
+
export declare function inferFilename(source: string, given?: string): string;
|
|
27
|
+
export interface RunOptions {
|
|
28
|
+
version: AbapVersion;
|
|
29
|
+
/** abaplint rule config; merged over the preset. */
|
|
30
|
+
rules?: Record<string, unknown> | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* "style" — abaplint's default ruleset minus whole-program semantic
|
|
33
|
+
* checks, so isolated snippets don't drown in noise about
|
|
34
|
+
* objects that simply weren't provided.
|
|
35
|
+
* "full" — abaplint's default ruleset as-is (expects you to provide
|
|
36
|
+
* every referenced dev object).
|
|
37
|
+
* "syntax-only" — parser + CDS parser errors only.
|
|
38
|
+
*/
|
|
39
|
+
preset: "style" | "full" | "syntax-only";
|
|
40
|
+
}
|
|
41
|
+
export interface RunResult {
|
|
42
|
+
findings: Finding[];
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
fileCount: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function runAbaplint(files: AbapSource[], opts: RunOptions): RunResult;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abaplint engine wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Design rules:
|
|
5
|
+
* - A fresh in-memory Registry per call — no shared state, safe under
|
|
6
|
+
* concurrent tool calls (pattern proven in RAP Dojo's /api/lint-abap).
|
|
7
|
+
* - abaplint PARSES the input, it never executes it. Inputs are still
|
|
8
|
+
* bounded (file count / chars) to keep each call cheap.
|
|
9
|
+
* - Filenames drive abaplint's object typing (zcl_x.clas.abap = a class).
|
|
10
|
+
* Callers may omit them; we infer from the source's leading statement.
|
|
11
|
+
*/
|
|
12
|
+
import * as abaplint from "@abaplint/core";
|
|
13
|
+
export const MAX_FILES = 32;
|
|
14
|
+
export const MAX_FILE_CHARS = 100_000;
|
|
15
|
+
export const MAX_FINDINGS = 500;
|
|
16
|
+
/** ABAP language versions a caller may target. */
|
|
17
|
+
export const ABAP_VERSIONS = [
|
|
18
|
+
"Cloud",
|
|
19
|
+
"v750",
|
|
20
|
+
"v751",
|
|
21
|
+
"v752",
|
|
22
|
+
"v753",
|
|
23
|
+
"v754",
|
|
24
|
+
"v755",
|
|
25
|
+
"v756",
|
|
26
|
+
"v757",
|
|
27
|
+
"v758",
|
|
28
|
+
];
|
|
29
|
+
const FILENAME_RE = /^[a-zA-Z0-9_#-]+\.(clas\.abap|clas\.locals_imp\.abap|clas\.locals_def\.abap|clas\.testclasses\.abap|prog\.abap|intf\.abap|fugr\.abap|ddls\.asddls|bdef\.asbdef|srvd\.srvdsrv|ddlx\.asddlx)$/;
|
|
30
|
+
/**
|
|
31
|
+
* Infer an abapGit-conventional filename from the source's first meaningful
|
|
32
|
+
* statement, so agents can lint snippets without knowing the convention.
|
|
33
|
+
*/
|
|
34
|
+
export function inferFilename(source, given) {
|
|
35
|
+
if (given !== undefined) {
|
|
36
|
+
if (!FILENAME_RE.test(given)) {
|
|
37
|
+
throw new Error(`Filename "${given}" is not an abapGit-style name (e.g. zcl_foo.clas.abap, zfoo.prog.abap, zr_foo.ddls.asddls).`);
|
|
38
|
+
}
|
|
39
|
+
return given.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
const head = source
|
|
42
|
+
.split("\n")
|
|
43
|
+
.map((l) => l.trim())
|
|
44
|
+
.filter((l) => l.length > 0 && !l.startsWith("*") && !l.startsWith('"'))
|
|
45
|
+
.slice(0, 5)
|
|
46
|
+
.join("\n");
|
|
47
|
+
const classMatch = /^\s*CLASS\s+(\w+)\s+DEFINITION/im.exec(head);
|
|
48
|
+
if (classMatch?.[1] !== undefined)
|
|
49
|
+
return `${classMatch[1].toLowerCase()}.clas.abap`;
|
|
50
|
+
const intfMatch = /^\s*INTERFACE\s+(\w+)/im.exec(head);
|
|
51
|
+
if (intfMatch?.[1] !== undefined)
|
|
52
|
+
return `${intfMatch[1].toLowerCase()}.intf.abap`;
|
|
53
|
+
if (/^\s*(@\w|define\s+(root\s+)?view)/im.test(head))
|
|
54
|
+
return "zsnippet.ddls.asddls";
|
|
55
|
+
if (/^\s*(managed|unmanaged|abstract|projection|interface;)/im.test(head))
|
|
56
|
+
return "zsnippet.bdef.asbdef";
|
|
57
|
+
const progMatch = /^\s*(REPORT|PROGRAM)\s+(\w+)/im.exec(head);
|
|
58
|
+
if (progMatch?.[2] !== undefined)
|
|
59
|
+
return `${progMatch[2].toLowerCase()}.prog.abap`;
|
|
60
|
+
return "zsnippet.prog.abap";
|
|
61
|
+
}
|
|
62
|
+
function boundFiles(files) {
|
|
63
|
+
if (files.length === 0)
|
|
64
|
+
throw new Error("Provide at least one source file.");
|
|
65
|
+
if (files.length > MAX_FILES)
|
|
66
|
+
throw new Error(`At most ${MAX_FILES} files per call.`);
|
|
67
|
+
const used = new Set();
|
|
68
|
+
return files.map((f) => {
|
|
69
|
+
if (f.source.length > MAX_FILE_CHARS) {
|
|
70
|
+
throw new Error(`File ${f.filename ?? "(unnamed)"} exceeds ${MAX_FILE_CHARS} characters; split it or lint the relevant part.`);
|
|
71
|
+
}
|
|
72
|
+
let filename = inferFilename(f.source, f.filename);
|
|
73
|
+
if (used.has(filename)) {
|
|
74
|
+
// Identical names would silently overwrite each other in the registry.
|
|
75
|
+
// Generic snippet fallbacks get a unique suffix; names derived from a
|
|
76
|
+
// declaration (or given explicitly) are a caller error — two files
|
|
77
|
+
// can't define the same object.
|
|
78
|
+
if (f.filename === undefined && filename.startsWith("zsnippet.")) {
|
|
79
|
+
let n = 2;
|
|
80
|
+
while (used.has(filename.replace("zsnippet.", `zsnippet${n}.`)))
|
|
81
|
+
n += 1;
|
|
82
|
+
filename = filename.replace("zsnippet.", `zsnippet${n}.`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw new Error(`Duplicate filename "${filename}" — each file must define a distinct object; pass unique filenames.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
used.add(filename);
|
|
89
|
+
return { filename, source: f.source };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function buildConfig(opts) {
|
|
93
|
+
let raw;
|
|
94
|
+
if (opts.preset === "syntax-only") {
|
|
95
|
+
raw = {
|
|
96
|
+
global: { files: "/**/*" },
|
|
97
|
+
syntax: { version: opts.version, errorNamespace: "^(Z|Y)" },
|
|
98
|
+
rules: { parser_error: true, cds_parser_error: true },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const def = abaplint.Config.getDefault().get();
|
|
103
|
+
def.syntax.version = opts.version;
|
|
104
|
+
if (opts.preset === "style") {
|
|
105
|
+
// Semantic whole-program checks false-positive on isolated snippets.
|
|
106
|
+
def.rules["check_syntax"] = false;
|
|
107
|
+
}
|
|
108
|
+
raw = def;
|
|
109
|
+
}
|
|
110
|
+
if (opts.rules !== undefined) {
|
|
111
|
+
const rules = raw["rules"];
|
|
112
|
+
for (const [k, v] of Object.entries(opts.rules))
|
|
113
|
+
rules[k] = v;
|
|
114
|
+
}
|
|
115
|
+
return new abaplint.Config(JSON.stringify(raw));
|
|
116
|
+
}
|
|
117
|
+
export function runAbaplint(files, opts) {
|
|
118
|
+
const bounded = boundFiles(files);
|
|
119
|
+
const registry = new abaplint.Registry(buildConfig(opts));
|
|
120
|
+
const lines = new Map();
|
|
121
|
+
for (const f of bounded) {
|
|
122
|
+
registry.addFile(new abaplint.MemoryFile(f.filename, f.source));
|
|
123
|
+
lines.set(f.filename, f.source.split("\n"));
|
|
124
|
+
}
|
|
125
|
+
registry.parse();
|
|
126
|
+
const issues = registry.findIssues();
|
|
127
|
+
const findings = issues.slice(0, MAX_FINDINGS).map((issue) => {
|
|
128
|
+
const start = issue.getStart();
|
|
129
|
+
const fileLines = lines.get(issue.getFilename());
|
|
130
|
+
const excerpt = (fileLines?.[start.getRow() - 1] ?? "").trim().slice(0, 100);
|
|
131
|
+
return {
|
|
132
|
+
rule: issue.getKey(),
|
|
133
|
+
message: issue.getMessage(),
|
|
134
|
+
severity: String(issue.getSeverity()),
|
|
135
|
+
file: issue.getFilename(),
|
|
136
|
+
line: start.getRow(),
|
|
137
|
+
column: start.getCol(),
|
|
138
|
+
excerpt,
|
|
139
|
+
docsUrl: `https://rules.abaplint.org/${issue.getKey()}/`,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
return { findings, truncated: issues.length > MAX_FINDINGS, fileCount: bounded.length };
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatAbap(source: string, filename?: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-print ABAP via abaplint's PrettyPrinter (keyword casing + indent).
|
|
3
|
+
* Only meaningful for ABAP sources — CDS/BDEF artifacts are not reformatted.
|
|
4
|
+
*/
|
|
5
|
+
import * as abaplint from "@abaplint/core";
|
|
6
|
+
import { invalidInput } from "../errors.js";
|
|
7
|
+
import { inferFilename, MAX_FILE_CHARS } from "./engine.js";
|
|
8
|
+
export function formatAbap(source, filename) {
|
|
9
|
+
if (source.length > MAX_FILE_CHARS) {
|
|
10
|
+
throw invalidInput(`Source exceeds ${MAX_FILE_CHARS} characters.`);
|
|
11
|
+
}
|
|
12
|
+
const name = inferFilename(source, filename);
|
|
13
|
+
if (!name.endsWith(".abap")) {
|
|
14
|
+
throw invalidInput(`format_abap handles ABAP sources only (got "${name}"). CDS and behavior definitions are not reformatted.`);
|
|
15
|
+
}
|
|
16
|
+
const config = abaplint.Config.getDefault();
|
|
17
|
+
const registry = new abaplint.Registry(config);
|
|
18
|
+
registry.addFile(new abaplint.MemoryFile(name, source));
|
|
19
|
+
registry.parse();
|
|
20
|
+
const obj = registry.getFirstObject();
|
|
21
|
+
if (!(obj instanceof abaplint.ABAPObject)) {
|
|
22
|
+
throw invalidInput("Could not parse the source as an ABAP object.");
|
|
23
|
+
}
|
|
24
|
+
const file = obj.getABAPFiles()[0];
|
|
25
|
+
if (file === undefined) {
|
|
26
|
+
throw invalidInput("Could not parse the source as an ABAP object.");
|
|
27
|
+
}
|
|
28
|
+
// "Fails cleanly on source it cannot parse" is the tool contract — returning
|
|
29
|
+
// broken code as "formatted" would launder syntax errors. Gate on the
|
|
30
|
+
// parse/structure issues only (style findings are not format blockers).
|
|
31
|
+
const SYNTAX_KEYS = new Set(["parser_error", "structure", "cds_parser_error"]);
|
|
32
|
+
const syntaxIssues = registry.findIssues().filter((i) => SYNTAX_KEYS.has(i.getKey()));
|
|
33
|
+
const firstIssue = syntaxIssues[0];
|
|
34
|
+
if (firstIssue !== undefined) {
|
|
35
|
+
throw invalidInput(`Source does not parse cleanly (${syntaxIssues.length} syntax/structure issue(s)); fix before formatting. First: line ${firstIssue.getStart().getRow()}: ${firstIssue.getMessage()}`);
|
|
36
|
+
}
|
|
37
|
+
return new abaplint.PrettyPrinter(file, config).run();
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AbapSource } from "./engine.js";
|
|
2
|
+
export interface MethodOutline {
|
|
3
|
+
name: string;
|
|
4
|
+
visibility: "public" | "protected" | "private";
|
|
5
|
+
}
|
|
6
|
+
export interface ClassOutline {
|
|
7
|
+
name: string;
|
|
8
|
+
isGlobal: boolean;
|
|
9
|
+
isFinal: boolean;
|
|
10
|
+
isAbstract: boolean;
|
|
11
|
+
isForTesting: boolean;
|
|
12
|
+
superClass: string | null;
|
|
13
|
+
interfaces: string[];
|
|
14
|
+
methods: MethodOutline[];
|
|
15
|
+
attributes: string[];
|
|
16
|
+
constants: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface FileOutline {
|
|
19
|
+
file: string;
|
|
20
|
+
classes: ClassOutline[];
|
|
21
|
+
interfaces: string[];
|
|
22
|
+
forms: string[];
|
|
23
|
+
parseable: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function outlineAbap(files: AbapSource[]): FileOutline[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural outline of ABAP sources — lets an agent navigate a large class
|
|
3
|
+
* or legacy include without reading every line into context.
|
|
4
|
+
*/
|
|
5
|
+
import * as abaplint from "@abaplint/core";
|
|
6
|
+
import { inferFilename, MAX_FILE_CHARS, MAX_FILES } from "./engine.js";
|
|
7
|
+
import { invalidInput } from "../errors.js";
|
|
8
|
+
const VISIBILITY = {
|
|
9
|
+
1: "private",
|
|
10
|
+
2: "protected",
|
|
11
|
+
3: "public",
|
|
12
|
+
};
|
|
13
|
+
export function outlineAbap(files) {
|
|
14
|
+
if (files.length === 0)
|
|
15
|
+
throw invalidInput("Provide at least one source file.");
|
|
16
|
+
if (files.length > MAX_FILES)
|
|
17
|
+
throw invalidInput(`At most ${MAX_FILES} files per call.`);
|
|
18
|
+
const config = abaplint.Config.getDefault();
|
|
19
|
+
const registry = new abaplint.Registry(config);
|
|
20
|
+
const names = [];
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
if (f.source.length > MAX_FILE_CHARS) {
|
|
23
|
+
throw invalidInput(`File ${f.filename ?? "(unnamed)"} exceeds ${MAX_FILE_CHARS} characters.`);
|
|
24
|
+
}
|
|
25
|
+
const name = inferFilename(f.source, f.filename);
|
|
26
|
+
names.push(name);
|
|
27
|
+
registry.addFile(new abaplint.MemoryFile(name, f.source));
|
|
28
|
+
}
|
|
29
|
+
registry.parse();
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
let found = false;
|
|
33
|
+
for (const obj of registry.getObjects()) {
|
|
34
|
+
if (!(obj instanceof abaplint.ABAPObject))
|
|
35
|
+
continue;
|
|
36
|
+
for (const file of obj.getABAPFiles()) {
|
|
37
|
+
if (file.getFilename() !== name)
|
|
38
|
+
continue;
|
|
39
|
+
found = true;
|
|
40
|
+
const info = file.getInfo();
|
|
41
|
+
out.push({
|
|
42
|
+
file: name,
|
|
43
|
+
parseable: true,
|
|
44
|
+
classes: info.listClassDefinitions().map((c) => ({
|
|
45
|
+
name: c.name,
|
|
46
|
+
isGlobal: c.isGlobal,
|
|
47
|
+
isFinal: c.isFinal,
|
|
48
|
+
isAbstract: c.isAbstract,
|
|
49
|
+
isForTesting: c.isForTesting,
|
|
50
|
+
superClass: c.superClassName?.toLowerCase() ?? null,
|
|
51
|
+
interfaces: c.interfaces.map((i) => i.name.toLowerCase()),
|
|
52
|
+
methods: c.methods.map((m) => ({
|
|
53
|
+
name: m.name,
|
|
54
|
+
visibility: VISIBILITY[m.visibility] ?? "private",
|
|
55
|
+
})),
|
|
56
|
+
attributes: c.attributes.map((a) => a.name),
|
|
57
|
+
constants: c.constants.map((k) => k.name),
|
|
58
|
+
})),
|
|
59
|
+
interfaces: info.listInterfaceDefinitions().map((i) => i.name),
|
|
60
|
+
forms: info.listFormDefinitions().map((f) => f.name),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!found) {
|
|
65
|
+
// Non-ABAP artifacts (CDS, BDEF) or unparseable sources get an empty outline.
|
|
66
|
+
out.push({ file: name, parseable: false, classes: [], interfaces: [], forms: [] });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP Cloud / Clean Core readiness — the dual-run diff.
|
|
3
|
+
*
|
|
4
|
+
* One abaplint pass at `version: Cloud` and one at a classic baseline
|
|
5
|
+
* (default v758). A finding present at Cloud but absent at the baseline is a
|
|
6
|
+
* *cloud blocker* (the statement is fine classic ABAP that ABAP Cloud no
|
|
7
|
+
* longer allows). A finding present at the baseline is just *broken code* —
|
|
8
|
+
* reporting it as a migration item would overstate the migration.
|
|
9
|
+
*
|
|
10
|
+
* Honest scope: this is the static, parser-level slice of readiness. The
|
|
11
|
+
* other half — "does this call only RELEASED SAP APIs?" — requires the
|
|
12
|
+
* system's released-API list (ATC check SAP_CP_READINESS) and is out of
|
|
13
|
+
* scope for an offline tool. The report says so.
|
|
14
|
+
*/
|
|
15
|
+
import type { AbapSource, AbapVersion, Finding } from "./engine.js";
|
|
16
|
+
export interface ReadinessCategory {
|
|
17
|
+
category: string;
|
|
18
|
+
label: string;
|
|
19
|
+
count: number;
|
|
20
|
+
findings: Finding[];
|
|
21
|
+
}
|
|
22
|
+
export interface ReadinessReport {
|
|
23
|
+
verdict: "ready" | "minor-rework" | "moderate-rework" | "significant-rework";
|
|
24
|
+
score: number;
|
|
25
|
+
cloudBlockerCount: number;
|
|
26
|
+
categories: ReadinessCategory[];
|
|
27
|
+
/** Findings that fail even at the classic baseline — fix these first; they are not migration items. */
|
|
28
|
+
brokenAtBaseline: Finding[];
|
|
29
|
+
baselineVersion: AbapVersion;
|
|
30
|
+
scopeNote: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const SCOPE_NOTE: string;
|
|
33
|
+
export declare function checkCloudReadiness(files: AbapSource[], baselineVersion?: AbapVersion): ReadinessReport;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { runAbaplint } from "./engine.js";
|
|
2
|
+
export const SCOPE_NOTE = "Static parser-level analysis (abaplint). It detects statements and syntax that ABAP Cloud removes, " +
|
|
3
|
+
"but NOT usage of unreleased SAP APIs — that requires a system's released-API list (ATC / SAP_CP_READINESS). " +
|
|
4
|
+
"Treat 'ready' as 'no language-level blockers', not as a full Clean Core certification.";
|
|
5
|
+
/** Map an offending line to a human category by its leading keyword(s). */
|
|
6
|
+
function categorize(excerpt) {
|
|
7
|
+
const head = excerpt.toUpperCase();
|
|
8
|
+
if (/^(WRITE|ULINE|SKIP|FORMAT|NEW-PAGE|TOP-OF-PAGE|END-OF-PAGE|PRINT-CONTROL)\b/.test(head))
|
|
9
|
+
return { category: "list-output", label: "Classic list output (WRITE…) — no UI in ABAP Cloud; expose data via RAP/OData instead" };
|
|
10
|
+
if (/^(CALL SCREEN|SET SCREEN|LEAVE SCREEN|MODULE|LOOP AT SCREEN|SET PF-STATUS|SET TITLEBAR|CALL DIALOG|SUPPRESS DIALOG)\b/.test(head))
|
|
11
|
+
return { category: "dynpro", label: "Dynpro / classic UI — rebuild the UI as a Fiori app on a RAP service" };
|
|
12
|
+
if (/^(SELECT-OPTIONS|PARAMETERS|SELECTION-SCREEN|AT SELECTION-SCREEN|INITIALIZATION|START-OF-SELECTION|END-OF-SELECTION|AT LINE-SELECTION|AT USER-COMMAND)\b/.test(head))
|
|
13
|
+
return { category: "report-events", label: "Report / selection-screen events — wrap the logic in a class; use RAP or an application job" };
|
|
14
|
+
if (/^(REPORT|PROGRAM|SUBMIT)\b/.test(head))
|
|
15
|
+
return { category: "report-program", label: "Executable program statements — ABAP Cloud has classes only; SUBMIT has no released equivalent" };
|
|
16
|
+
if (/^(EXEC SQL|ENDEXEC)\b/.test(head))
|
|
17
|
+
return { category: "native-sql", label: "Native SQL — use ABAP SQL (or AMDP where released)" };
|
|
18
|
+
if (/^(CALL FUNCTION .*DESTINATION|CALL FUNCTION .*STARTING NEW TASK|RECEIVE RESULTS)\b/.test(head))
|
|
19
|
+
return { category: "rfc", label: "Direct RFC patterns — use released connectivity (bgPF, HTTP, released RFC wrappers)" };
|
|
20
|
+
if (/^(FORM|PERFORM|ENDFORM)\b/.test(head))
|
|
21
|
+
return { category: "subroutines", label: "FORM subroutines — obsolete; move logic into class methods" };
|
|
22
|
+
if (/^(CALL TRANSACTION|LEAVE TO TRANSACTION|SET PARAMETER|GET PARAMETER|AUTHORITY-CHECK)\b/.test(head))
|
|
23
|
+
return { category: "transaction-glue", label: "Transaction / SPA-GPA / classic auth glue — re-model on released APIs" };
|
|
24
|
+
return { category: "other", label: "Other statements ABAP Cloud does not allow" };
|
|
25
|
+
}
|
|
26
|
+
export function checkCloudReadiness(files, baselineVersion = "v758") {
|
|
27
|
+
const cloud = runAbaplint(files, { version: "Cloud", preset: "syntax-only" });
|
|
28
|
+
const baseline = runAbaplint(files, { version: baselineVersion, preset: "syntax-only" });
|
|
29
|
+
const baselineKeys = new Set(baseline.findings.map((f) => `${f.file}:${f.line}:${f.rule}`));
|
|
30
|
+
const blockers = [];
|
|
31
|
+
for (const f of cloud.findings) {
|
|
32
|
+
if (!baselineKeys.has(`${f.file}:${f.line}:${f.rule}`))
|
|
33
|
+
blockers.push(f);
|
|
34
|
+
}
|
|
35
|
+
const byCategory = new Map();
|
|
36
|
+
for (const f of blockers) {
|
|
37
|
+
const { category, label } = categorize(f.excerpt);
|
|
38
|
+
const entry = byCategory.get(category) ?? { category, label, count: 0, findings: [] };
|
|
39
|
+
entry.count += 1;
|
|
40
|
+
entry.findings.push(f);
|
|
41
|
+
byCategory.set(category, entry);
|
|
42
|
+
}
|
|
43
|
+
const n = blockers.length;
|
|
44
|
+
// Transparent, documented formula — a conversation starter, not an oracle.
|
|
45
|
+
const score = Math.max(0, 100 - 5 * n);
|
|
46
|
+
const verdict = n === 0 ? "ready" : n <= 5 ? "minor-rework" : n <= 20 ? "moderate-rework" : "significant-rework";
|
|
47
|
+
return {
|
|
48
|
+
verdict,
|
|
49
|
+
score,
|
|
50
|
+
cloudBlockerCount: n,
|
|
51
|
+
categories: [...byCategory.values()].sort((a, b) => b.count - a.count),
|
|
52
|
+
brokenAtBaseline: baseline.findings,
|
|
53
|
+
baselineVersion,
|
|
54
|
+
scopeNote: SCOPE_NOTE,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RuleSummary {
|
|
2
|
+
key: string;
|
|
3
|
+
title: string;
|
|
4
|
+
shortDescription: string;
|
|
5
|
+
tags: string[];
|
|
6
|
+
docsUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RuleDetail extends RuleSummary {
|
|
9
|
+
extendedInformation: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listRules(query?: string, tag?: string): RuleSummary[];
|
|
12
|
+
export declare function explainRule(key: string): RuleDetail & {
|
|
13
|
+
badExample?: string;
|
|
14
|
+
goodExample?: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule catalog — thin projection over abaplint's own rule metadata, so the
|
|
3
|
+
* documentation an agent reads is the analyzer's, not a copy that can drift.
|
|
4
|
+
*/
|
|
5
|
+
import * as abaplint from "@abaplint/core";
|
|
6
|
+
import { notFound } from "../errors.js";
|
|
7
|
+
function metadata() {
|
|
8
|
+
return abaplint.ArtifactsRules.getRules().map((r) => r.getMetadata());
|
|
9
|
+
}
|
|
10
|
+
function toSummary(m) {
|
|
11
|
+
return {
|
|
12
|
+
key: m.key,
|
|
13
|
+
title: m.title,
|
|
14
|
+
shortDescription: m.shortDescription,
|
|
15
|
+
tags: m.tags ?? [],
|
|
16
|
+
docsUrl: `https://rules.abaplint.org/${m.key}/`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function listRules(query, tag) {
|
|
20
|
+
const q = query?.toLowerCase();
|
|
21
|
+
return metadata()
|
|
22
|
+
.filter((m) => {
|
|
23
|
+
if (tag !== undefined && !(m.tags ?? []).some((t) => t.toLowerCase() === tag.toLowerCase()))
|
|
24
|
+
return false;
|
|
25
|
+
if (q !== undefined) {
|
|
26
|
+
const hay = `${m.key} ${m.title} ${m.shortDescription}`.toLowerCase();
|
|
27
|
+
if (!hay.includes(q))
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
})
|
|
32
|
+
.map(toSummary)
|
|
33
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
34
|
+
}
|
|
35
|
+
export function explainRule(key) {
|
|
36
|
+
const m = metadata().find((x) => x.key === key.toLowerCase());
|
|
37
|
+
if (m === undefined) {
|
|
38
|
+
throw notFound(`No abaplint rule named "${key}". Use list_abap_rules to browse valid keys.`, {
|
|
39
|
+
key,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const detail = {
|
|
43
|
+
...toSummary(m),
|
|
44
|
+
extendedInformation: m.extendedInformation ?? "",
|
|
45
|
+
};
|
|
46
|
+
if (m.badExample !== undefined)
|
|
47
|
+
detail.badExample = m.badExample;
|
|
48
|
+
if (m.goodExample !== undefined)
|
|
49
|
+
detail.goodExample = m.goodExample;
|
|
50
|
+
return detail;
|
|
51
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAP managed-BO scaffolder.
|
|
3
|
+
*
|
|
4
|
+
* Generates the canonical ABAP-Cloud stack for one root entity — the same
|
|
5
|
+
* shape as SAP's /DMO reference apps and the ADT wizards:
|
|
6
|
+
*
|
|
7
|
+
* root view entity (ZR_) → behavior definition (managed, strict(2))
|
|
8
|
+
* projection view (ZC_) → projection behavior definition
|
|
9
|
+
* behavior implementation class (ZBP_) + handler locals
|
|
10
|
+
* metadata extension (UI annotations) + service definition (ZUI_…_V4)
|
|
11
|
+
*
|
|
12
|
+
* Everything the generator emits that abaplint can parse is round-tripped
|
|
13
|
+
* through abaplint at version Cloud before it is returned: the generator and
|
|
14
|
+
* the linter share one parser, so the scaffold can never drift into syntax
|
|
15
|
+
* the lint would reject. BDEF/SRVD artifacts are outside abaplint's checked
|
|
16
|
+
* surface (verified empirically) — those are covered by golden tests and
|
|
17
|
+
* marked validated:"template".
|
|
18
|
+
*/
|
|
19
|
+
import type { Finding } from "./engine.js";
|
|
20
|
+
export interface ScaffoldField {
|
|
21
|
+
/** snake_case table field name, e.g. "agency_id". */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Suggested DDL type for the table source, e.g. "abap.char(6)". */
|
|
24
|
+
type?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
export interface ScaffoldOptions {
|
|
27
|
+
/** Entity in UpperCamelCase, e.g. "Travel". Drives all artifact names. */
|
|
28
|
+
entityName: string;
|
|
29
|
+
/** Persistent SQL table, e.g. "ztravel". */
|
|
30
|
+
sqlTable: string;
|
|
31
|
+
/** snake_case key field name, e.g. "travel_id". */
|
|
32
|
+
keyField: string;
|
|
33
|
+
/** True (default): UUID key, managed numbering. False: caller supplies the key on create. */
|
|
34
|
+
managedUuidKey: boolean;
|
|
35
|
+
fields: ScaffoldField[];
|
|
36
|
+
draft: boolean;
|
|
37
|
+
/** Dev namespace prefix, "Z" or "Y". */
|
|
38
|
+
prefix: "Z" | "Y";
|
|
39
|
+
}
|
|
40
|
+
export interface ScaffoldFile {
|
|
41
|
+
filename: string;
|
|
42
|
+
content: string;
|
|
43
|
+
/** "abaplint" = machine-validated through the parser; "template" = golden-tested template. */
|
|
44
|
+
validated: "abaplint" | "template";
|
|
45
|
+
}
|
|
46
|
+
export interface ScaffoldResult {
|
|
47
|
+
files: ScaffoldFile[];
|
|
48
|
+
activationOrder: string[];
|
|
49
|
+
nextSteps: string[];
|
|
50
|
+
suggestedTableDdl: string;
|
|
51
|
+
/** Non-empty only if the generated sources failed abaplint — should never happen. */
|
|
52
|
+
validationIssues: Finding[];
|
|
53
|
+
}
|
|
54
|
+
export declare function snakeToCamel(snake: string): string;
|
|
55
|
+
export declare function scaffoldRapBo(opts: ScaffoldOptions): ScaffoldResult;
|