Skip to content

Commit bc378ea

Browse files
feat(openai-codegen): Wave 1 — AST nodes, OAS loader, target profiles
- abap-ast: typed node interfaces + factories for classes, interfaces, types, methods, statements, expressions, data declarations (59 unit tests). - openai-codegen: OAS loader (swagger-parser + yaml) + normalizer that merges path-level parameters, synthesises missing operationIds, classifies 2xx/4xx/5xx responses, and preserves named components.schemas before dereferencing (11 tests, exercised against vendored Petstore v3 fixture). - openai-codegen: target profile registry with full s4-cloud definition (web HTTP client + inline JSON) and stubs for on-prem profiles; class whitelist enforcement with WhitelistViolationError (5 tests). All four verification targets (typecheck, test, build, lint) pass on both packages. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent b6aa7c8 commit bc378ea

25 files changed

Lines changed: 3060 additions & 0 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { AbapAstError } from './errors';
2+
3+
/** All node kinds in the AST. */
4+
export type NodeKind =
5+
// types
6+
| 'BuiltinType'
7+
| 'TableType'
8+
| 'StructureType'
9+
| 'NamedTypeRef'
10+
| 'EnumType'
11+
| 'TypeDef'
12+
// data
13+
| 'DataDecl'
14+
| 'ConstantDecl'
15+
| 'FieldSymbolDecl'
16+
// statements
17+
| 'Assign'
18+
| 'Call'
19+
| 'Raise'
20+
| 'If'
21+
| 'Loop'
22+
| 'Return'
23+
| 'Try'
24+
| 'Append'
25+
| 'Insert'
26+
| 'Read'
27+
| 'Clear'
28+
| 'Exit'
29+
| 'Continue'
30+
| 'Raw'
31+
// expressions
32+
| 'Literal'
33+
| 'IdentifierExpr'
34+
| 'ConstructorExpr'
35+
| 'MethodCallExpr'
36+
| 'BinOp'
37+
| 'StringTemplate'
38+
| 'Cast'
39+
// members
40+
| 'MethodParam'
41+
| 'MethodDef'
42+
| 'MethodImpl'
43+
| 'EventDef'
44+
| 'AttributeDef'
45+
// class / interface
46+
| 'Section'
47+
| 'ClassDef'
48+
| 'LocalClassDef'
49+
| 'InterfaceDef'
50+
// shared
51+
| 'Comment';
52+
53+
/** Base shape for any AST node. */
54+
export interface AbapNode {
55+
readonly kind: NodeKind;
56+
}
57+
58+
/** An ABAP identifier (not validated beyond presence). */
59+
export type Identifier = string;
60+
61+
/** An ABAP comment. Star comments begin at column 1; line comments use `"`. */
62+
export interface Comment extends AbapNode {
63+
readonly kind: 'Comment';
64+
readonly text: string;
65+
readonly style: 'star' | 'line';
66+
}
67+
68+
export function comment(input: {
69+
text: string;
70+
style?: 'star' | 'line';
71+
}): Comment {
72+
if (typeof input.text !== 'string') {
73+
throw new AbapAstError('Comment: required field "text" is missing');
74+
}
75+
return Object.freeze({
76+
kind: 'Comment' as const,
77+
text: input.text,
78+
style: input.style ?? 'line',
79+
});
80+
}
81+
82+
/** Visibility modifier for class members / sections. */
83+
export type Visibility = 'public' | 'protected' | 'private';
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { AbapNode, Identifier, Visibility } from './base';
2+
import { AbapAstError } from './errors';
3+
import type { TypeDef } from './types';
4+
import type { ConstantDecl } from './data';
5+
import type { AttributeDef, EventDef, MethodDef, MethodImpl } from './members';
6+
7+
/** A member that may appear inside a class section. */
8+
export type SectionMember =
9+
| TypeDef
10+
| AttributeDef
11+
| MethodDef
12+
| ConstantDecl
13+
| EventDef;
14+
15+
/** A visibility section of a class definition. */
16+
export interface Section extends AbapNode {
17+
readonly kind: 'Section';
18+
readonly visibility: Visibility;
19+
readonly members: readonly SectionMember[];
20+
}
21+
22+
export function section(input: {
23+
visibility: Visibility;
24+
members?: readonly SectionMember[];
25+
}): Section {
26+
if (!input.visibility) {
27+
throw new AbapAstError('Section: required field "visibility" is missing');
28+
}
29+
const members = input.members ?? [];
30+
for (const m of members) {
31+
if ('visibility' in m && m.visibility !== input.visibility) {
32+
throw new AbapAstError(
33+
`Section: member "${m.name}" has visibility "${m.visibility}" but section is "${input.visibility}"`,
34+
);
35+
}
36+
}
37+
return Object.freeze({
38+
kind: 'Section' as const,
39+
visibility: input.visibility,
40+
members: Object.freeze([...members]),
41+
});
42+
}
43+
44+
/** Top-level global class (`CLASS ... DEFINITION` + `IMPLEMENTATION`). */
45+
export interface ClassDef extends AbapNode {
46+
readonly kind: 'ClassDef';
47+
readonly name: Identifier;
48+
readonly superclass?: Identifier;
49+
readonly interfaces: readonly Identifier[];
50+
readonly isFinal?: boolean;
51+
readonly isAbstract?: boolean;
52+
readonly isForTesting?: boolean;
53+
readonly isCreatePrivate?: boolean;
54+
readonly sections: readonly Section[];
55+
readonly implementations: readonly MethodImpl[];
56+
}
57+
58+
export function classDef(input: {
59+
name: Identifier;
60+
superclass?: Identifier;
61+
interfaces?: readonly Identifier[];
62+
isFinal?: boolean;
63+
isAbstract?: boolean;
64+
isForTesting?: boolean;
65+
isCreatePrivate?: boolean;
66+
sections?: readonly Section[];
67+
implementations?: readonly MethodImpl[];
68+
}): ClassDef {
69+
if (!input.name) {
70+
throw new AbapAstError('ClassDef: required field "name" is missing');
71+
}
72+
if (input.isFinal && input.isAbstract) {
73+
throw new AbapAstError('ClassDef: class cannot be both FINAL and ABSTRACT');
74+
}
75+
return Object.freeze({
76+
kind: 'ClassDef' as const,
77+
name: input.name,
78+
superclass: input.superclass,
79+
interfaces: Object.freeze([...(input.interfaces ?? [])]),
80+
isFinal: input.isFinal,
81+
isAbstract: input.isAbstract,
82+
isForTesting: input.isForTesting,
83+
isCreatePrivate: input.isCreatePrivate,
84+
sections: Object.freeze([...(input.sections ?? [])]),
85+
implementations: Object.freeze([...(input.implementations ?? [])]),
86+
});
87+
}
88+
89+
/** A local class inside a CLAS-POOL (same shape as ClassDef, flagged local). */
90+
export interface LocalClassDef extends AbapNode {
91+
readonly kind: 'LocalClassDef';
92+
readonly name: Identifier;
93+
readonly superclass?: Identifier;
94+
readonly interfaces: readonly Identifier[];
95+
readonly isFinal?: boolean;
96+
readonly isAbstract?: boolean;
97+
readonly isForTesting?: boolean;
98+
readonly sections: readonly Section[];
99+
readonly implementations: readonly MethodImpl[];
100+
readonly local: true;
101+
}
102+
103+
export function localClassDef(input: {
104+
name: Identifier;
105+
superclass?: Identifier;
106+
interfaces?: readonly Identifier[];
107+
isFinal?: boolean;
108+
isAbstract?: boolean;
109+
isForTesting?: boolean;
110+
sections?: readonly Section[];
111+
implementations?: readonly MethodImpl[];
112+
}): LocalClassDef {
113+
if (!input.name) {
114+
throw new AbapAstError('LocalClassDef: required field "name" is missing');
115+
}
116+
if (input.isFinal && input.isAbstract) {
117+
throw new AbapAstError(
118+
'LocalClassDef: class cannot be both FINAL and ABSTRACT',
119+
);
120+
}
121+
return Object.freeze({
122+
kind: 'LocalClassDef' as const,
123+
name: input.name,
124+
superclass: input.superclass,
125+
interfaces: Object.freeze([...(input.interfaces ?? [])]),
126+
isFinal: input.isFinal,
127+
isAbstract: input.isAbstract,
128+
isForTesting: input.isForTesting,
129+
sections: Object.freeze([...(input.sections ?? [])]),
130+
implementations: Object.freeze([...(input.implementations ?? [])]),
131+
local: true as const,
132+
});
133+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { AbapNode, Identifier } from './base';
2+
import { AbapAstError } from './errors';
3+
import type { TypeRef } from './types';
4+
import type { Expression } from './expressions';
5+
6+
/** `DATA lv_x TYPE ... [VALUE ...]`. */
7+
export interface DataDecl extends AbapNode {
8+
readonly kind: 'DataDecl';
9+
readonly name: Identifier;
10+
readonly type: TypeRef;
11+
readonly initial?: Expression;
12+
readonly classData?: boolean;
13+
}
14+
15+
export function dataDecl(input: {
16+
name: Identifier;
17+
type: TypeRef;
18+
initial?: Expression;
19+
classData?: boolean;
20+
}): DataDecl {
21+
if (!input.name) {
22+
throw new AbapAstError('DataDecl: required field "name" is missing');
23+
}
24+
if (!input.type) {
25+
throw new AbapAstError('DataDecl: required field "type" is missing');
26+
}
27+
return Object.freeze({
28+
kind: 'DataDecl' as const,
29+
name: input.name,
30+
type: input.type,
31+
initial: input.initial,
32+
classData: input.classData,
33+
});
34+
}
35+
36+
/** `CONSTANTS c_x TYPE ... VALUE ...`. */
37+
export interface ConstantDecl extends AbapNode {
38+
readonly kind: 'ConstantDecl';
39+
readonly name: Identifier;
40+
readonly type: TypeRef;
41+
readonly value: Expression;
42+
readonly classData?: boolean;
43+
}
44+
45+
export function constantDecl(input: {
46+
name: Identifier;
47+
type: TypeRef;
48+
value: Expression;
49+
classData?: boolean;
50+
}): ConstantDecl {
51+
if (!input.name) {
52+
throw new AbapAstError('ConstantDecl: required field "name" is missing');
53+
}
54+
if (!input.type) {
55+
throw new AbapAstError('ConstantDecl: required field "type" is missing');
56+
}
57+
if (!input.value) {
58+
throw new AbapAstError('ConstantDecl: required field "value" is missing');
59+
}
60+
return Object.freeze({
61+
kind: 'ConstantDecl' as const,
62+
name: input.name,
63+
type: input.type,
64+
value: input.value,
65+
classData: input.classData,
66+
});
67+
}
68+
69+
/** `FIELD-SYMBOLS <fs_x> TYPE ...`. */
70+
export interface FieldSymbolDecl extends AbapNode {
71+
readonly kind: 'FieldSymbolDecl';
72+
readonly name: Identifier;
73+
readonly type: TypeRef;
74+
}
75+
76+
export function fieldSymbolDecl(input: {
77+
name: Identifier;
78+
type: TypeRef;
79+
}): FieldSymbolDecl {
80+
if (!input.name) {
81+
throw new AbapAstError('FieldSymbolDecl: required field "name" is missing');
82+
}
83+
if (!input.name.startsWith('<') || !input.name.endsWith('>')) {
84+
throw new AbapAstError(
85+
`FieldSymbolDecl: name "${input.name}" must be wrapped in angle brackets (e.g. <fs_x>)`,
86+
);
87+
}
88+
if (!input.type) {
89+
throw new AbapAstError('FieldSymbolDecl: required field "type" is missing');
90+
}
91+
return Object.freeze({
92+
kind: 'FieldSymbolDecl' as const,
93+
name: input.name,
94+
type: input.type,
95+
});
96+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export class AbapAstError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = 'AbapAstError';
5+
}
6+
}
7+
8+
export function requireField<T>(
9+
value: T | undefined | null,
10+
field: string,
11+
nodeKind: string,
12+
): T {
13+
if (value === undefined || value === null || value === '') {
14+
throw new AbapAstError(`${nodeKind}: required field "${field}" is missing`);
15+
}
16+
return value;
17+
}

0 commit comments

Comments
 (0)