@abapify/adt-lint 0.0.0 → 0.4.4
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/README.md +12 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +269 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -1
package/README.md
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
1
|
# @abapify/adt-lint
|
|
2
|
+
|
|
3
|
+
Shared ABAP lint and source-shaping utilities built on top of `@abaplint/core`.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
- `lintSource(source, options)`
|
|
8
|
+
- `lintAndFix(source, options)`
|
|
9
|
+
- `listRules(options)`
|
|
10
|
+
- `buildPreset(systemType)`
|
|
11
|
+
- `stripToPublicApi(source, objectType)`
|
|
12
|
+
- `extractDependencies(source)`
|
|
13
|
+
- `detectMethodBoundary(source, methodName)`
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/lib/types.d.ts
|
|
2
|
+
type LintSeverity = 'error' | 'warning' | 'info';
|
|
3
|
+
interface LintDiagnostic {
|
|
4
|
+
key: string;
|
|
5
|
+
message: string;
|
|
6
|
+
severity: LintSeverity;
|
|
7
|
+
filename: string;
|
|
8
|
+
start: {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
};
|
|
12
|
+
end: {
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
type LintSystemType = 'btp' | 'onpremise';
|
|
18
|
+
interface LintOptions {
|
|
19
|
+
filename?: string;
|
|
20
|
+
systemType?: LintSystemType;
|
|
21
|
+
ruleOverrides?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
interface RuleInfo {
|
|
24
|
+
key: string;
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
config?: unknown;
|
|
27
|
+
}
|
|
28
|
+
interface MethodBoundary {
|
|
29
|
+
startLine: number;
|
|
30
|
+
endLine: number;
|
|
31
|
+
}
|
|
32
|
+
interface StripResult {
|
|
33
|
+
source: string;
|
|
34
|
+
fallback: boolean;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/lib/preset.d.ts
|
|
38
|
+
declare function buildPreset(systemType: LintSystemType): Record<string, unknown>;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/lib/lint.d.ts
|
|
41
|
+
declare function lintSource(source: string, options?: LintOptions): LintDiagnostic[];
|
|
42
|
+
declare function lintAndFix(source: string, options?: LintOptions): {
|
|
43
|
+
source: string;
|
|
44
|
+
remaining: LintDiagnostic[];
|
|
45
|
+
};
|
|
46
|
+
declare function listRules(options?: LintOptions): RuleInfo[];
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/lib/context.d.ts
|
|
49
|
+
type SourceObjectType = 'CLAS' | 'INTF' | 'FUNC' | 'DDLS' | 'PROG' | string;
|
|
50
|
+
declare function stripToPublicApi(source: string, objectType: SourceObjectType): StripResult;
|
|
51
|
+
declare function extractDependencies(source: string): string[];
|
|
52
|
+
declare function normalizeMethodBody(source: string, methodName: string): string;
|
|
53
|
+
declare function detectMethodBoundary(source: string, methodName: string): MethodBoundary | null;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { type LintDiagnostic, type LintOptions, type LintSeverity, type LintSystemType, type MethodBoundary, type RuleInfo, type StripResult, buildPreset, detectMethodBoundary, extractDependencies, lintAndFix, lintSource, listRules, normalizeMethodBody, stripToPublicApi };
|
|
56
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/preset.ts","../src/lib/lint.ts","../src/lib/context.ts"],"mappings":";KAAY;UAEK;EACf;EACA;EACA,UAAU;EACV;EACA;IACE;IACA;;EAEF;IACE;IACA;;;KAIQ;UAEK;EACf;EACA,aAAa;EACb,gBAAgB;;UAGD;EACf;EACA;EACA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;;;;iBC5Bc,YACd,YAAY,iBACX;;;iBCgEa,WACd,gBACA,UAAU,cACT;iBAKa,WACd,gBACA,UAAU;EAEV;EACA,WAAW;;iBAmBG,UAAU,UAAU,cAAc;;;KC1G7C;iBAkFW,iBACd,gBACA,YAAY,mBACX;iBA6Ba,oBAAoB;iBAsFpB,oBACd,gBACA;iBAgBc,qBACd,gBACA,qBACC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { ArtifactsRules, Config, Edits, MemoryFile, Registry, Severity } from "@abaplint/core";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/preset.ts
|
|
4
|
+
function cloneDefaultConfig() {
|
|
5
|
+
return JSON.parse(JSON.stringify(Config.getDefault().get()));
|
|
6
|
+
}
|
|
7
|
+
function buildPreset(systemType) {
|
|
8
|
+
const config = cloneDefaultConfig();
|
|
9
|
+
config.rules ??= {};
|
|
10
|
+
if (systemType === "btp") {
|
|
11
|
+
config.rules.cloud_types = {
|
|
12
|
+
...config.rules.cloud_types ?? {},
|
|
13
|
+
severity: "Error"
|
|
14
|
+
};
|
|
15
|
+
config.rules.strict_sql = {
|
|
16
|
+
...config.rules.strict_sql ?? {},
|
|
17
|
+
severity: "Error"
|
|
18
|
+
};
|
|
19
|
+
} else {
|
|
20
|
+
delete config.rules.cloud_types;
|
|
21
|
+
config.rules.obsolete_statement = {
|
|
22
|
+
...config.rules.obsolete_statement ?? {},
|
|
23
|
+
severity: "Warning"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/lib/lint.ts
|
|
31
|
+
const FALLBACK_FILENAME = "anonymous.prog.abap";
|
|
32
|
+
function toSeverity(severity) {
|
|
33
|
+
switch (severity) {
|
|
34
|
+
case Severity.Warning: return "warning";
|
|
35
|
+
case Severity.Info: return "info";
|
|
36
|
+
default: return "error";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function toDiagnostics(registry) {
|
|
40
|
+
return registry.findIssues().map((issue) => ({
|
|
41
|
+
key: issue.getKey(),
|
|
42
|
+
message: issue.getMessage(),
|
|
43
|
+
severity: toSeverity(issue.getSeverity()),
|
|
44
|
+
filename: issue.getFilename(),
|
|
45
|
+
start: {
|
|
46
|
+
line: issue.getStart().getRow(),
|
|
47
|
+
column: issue.getStart().getCol()
|
|
48
|
+
},
|
|
49
|
+
end: {
|
|
50
|
+
line: issue.getEnd().getRow(),
|
|
51
|
+
column: issue.getEnd().getCol()
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function buildConfig(options) {
|
|
56
|
+
const base = buildPreset(options?.systemType ?? "onpremise");
|
|
57
|
+
if (options?.ruleOverrides) {
|
|
58
|
+
base.rules ??= {};
|
|
59
|
+
base.rules = {
|
|
60
|
+
...base.rules,
|
|
61
|
+
...options.ruleOverrides
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return new Config(JSON.stringify(base));
|
|
65
|
+
}
|
|
66
|
+
function createRegistry(source, options) {
|
|
67
|
+
const filename = options?.filename ?? FALLBACK_FILENAME;
|
|
68
|
+
const config = buildConfig(options);
|
|
69
|
+
const registry = new Registry(config);
|
|
70
|
+
registry.addFile(new MemoryFile(filename, source));
|
|
71
|
+
registry.parse();
|
|
72
|
+
return {
|
|
73
|
+
registry,
|
|
74
|
+
filename,
|
|
75
|
+
config
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function lintSource(source, options) {
|
|
79
|
+
const { registry } = createRegistry(source, options);
|
|
80
|
+
return toDiagnostics(registry);
|
|
81
|
+
}
|
|
82
|
+
function lintAndFix(source, options) {
|
|
83
|
+
const { registry, filename } = createRegistry(source, options);
|
|
84
|
+
const fixes = registry.findIssues().map((issue) => issue.getDefaultFix()).filter((fix) => Boolean(fix));
|
|
85
|
+
if (fixes.length > 0) {
|
|
86
|
+
Edits.applyEditList(registry, fixes);
|
|
87
|
+
registry.parse();
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
source: registry.getFileByName(filename)?.getRaw() ?? source,
|
|
91
|
+
remaining: toDiagnostics(registry)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function listRules(options) {
|
|
95
|
+
const config = buildConfig(options);
|
|
96
|
+
const enabled = new Set(config.getEnabledRules().map((rule) => rule.getMetadata().key));
|
|
97
|
+
return ArtifactsRules.getRules().map((rule) => {
|
|
98
|
+
const key = rule.getMetadata().key;
|
|
99
|
+
return {
|
|
100
|
+
key,
|
|
101
|
+
enabled: enabled.has(key),
|
|
102
|
+
config: config.readByRule(key)
|
|
103
|
+
};
|
|
104
|
+
}).sort((a, b) => a.key.localeCompare(b.key));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/lib/context.ts
|
|
109
|
+
function stripClassSource(source) {
|
|
110
|
+
const lines = source.split(/\r?\n/);
|
|
111
|
+
const output = [];
|
|
112
|
+
let inDefinition = false;
|
|
113
|
+
let inHiddenSection = false;
|
|
114
|
+
let foundDefinition = false;
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const upper = line.trim().toUpperCase();
|
|
117
|
+
if (!inDefinition) {
|
|
118
|
+
if (upper.startsWith("CLASS ") && upper.includes(" DEFINITION") && upper.endsWith(".")) {
|
|
119
|
+
inDefinition = true;
|
|
120
|
+
foundDefinition = true;
|
|
121
|
+
output.push(line);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (upper === "PROTECTED SECTION." || upper === "PRIVATE SECTION.") {
|
|
126
|
+
inHiddenSection = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (upper === "PUBLIC SECTION.") {
|
|
130
|
+
inHiddenSection = false;
|
|
131
|
+
output.push(line);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (upper === "ENDCLASS.") {
|
|
135
|
+
output.push(line);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if (!inHiddenSection) output.push(line);
|
|
139
|
+
}
|
|
140
|
+
if (!foundDefinition || output.length === 0) return {
|
|
141
|
+
source,
|
|
142
|
+
fallback: true
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
source: `${output.join("\n").trim()}\n`,
|
|
146
|
+
fallback: false
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function stripFunctionSource(source) {
|
|
150
|
+
const lines = source.split(/\r?\n/);
|
|
151
|
+
const start = lines.findIndex((line) => /^\s*FUNCTION\s+/i.test(line));
|
|
152
|
+
const end = lines.findIndex((line, index) => index >= start && /^\s*ENDFUNCTION\./i.test(line));
|
|
153
|
+
if (start < 0 || end < 0) return {
|
|
154
|
+
source,
|
|
155
|
+
fallback: true
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
source: `${lines.slice(start, end + 1).filter((line) => {
|
|
159
|
+
const trimmed = line.trim().toUpperCase();
|
|
160
|
+
return trimmed.startsWith("FUNCTION ") || trimmed.startsWith("IMPORTING") || trimmed.startsWith("EXPORTING") || trimmed.startsWith("CHANGING") || trimmed.startsWith("TABLES") || trimmed.startsWith("EXCEPTIONS") || trimmed.startsWith("ENDFUNCTION.") || trimmed === "";
|
|
161
|
+
}).join("\n").trim()}\n`,
|
|
162
|
+
fallback: false
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function stripToPublicApi(source, objectType) {
|
|
166
|
+
const normalized = objectType.toUpperCase();
|
|
167
|
+
if (normalized === "INTF" || normalized === "DDLS" || normalized === "PROG") return {
|
|
168
|
+
source,
|
|
169
|
+
fallback: false
|
|
170
|
+
};
|
|
171
|
+
if (normalized === "CLAS") return stripClassSource(source);
|
|
172
|
+
if (normalized === "FUNC") return stripFunctionSource(source);
|
|
173
|
+
return {
|
|
174
|
+
source,
|
|
175
|
+
fallback: true
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizeToken(token) {
|
|
179
|
+
let t = token;
|
|
180
|
+
while (t.startsWith("'")) t = t.slice(1);
|
|
181
|
+
while (t.endsWith("'")) t = t.slice(0, -1);
|
|
182
|
+
return t.toUpperCase();
|
|
183
|
+
}
|
|
184
|
+
function isCustomName(token) {
|
|
185
|
+
return Boolean(token && /^[ZY][A-Z0-9_]*$/i.test(token));
|
|
186
|
+
}
|
|
187
|
+
function extractDependencies(source) {
|
|
188
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
189
|
+
const tokens = source.split(/\r?\n/).map((line) => {
|
|
190
|
+
const commentIndex = line.indexOf("\"");
|
|
191
|
+
return commentIndex >= 0 ? line.slice(0, commentIndex) : line;
|
|
192
|
+
}).join("\n").match(/=>|[A-Z_][A-Z0-9_]*|'[^']*'|[.,]/gi) ?? [];
|
|
193
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
194
|
+
const t0 = normalizeToken(tokens[i]);
|
|
195
|
+
const t1 = normalizeToken(tokens[i + 1] ?? "");
|
|
196
|
+
const t2 = normalizeToken(tokens[i + 2] ?? "");
|
|
197
|
+
const t3 = normalizeToken(tokens[i + 3] ?? "");
|
|
198
|
+
if (t0 === "TYPE" && t1 === "REF" && t2 === "TO" && isCustomName(t3)) {
|
|
199
|
+
dependencies.add(t3);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if ((t0 === "NEW" || t0 === "CAST") && isCustomName(t1)) {
|
|
203
|
+
dependencies.add(t1);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (t0 === "INHERITING" && t1 === "FROM" && isCustomName(t2)) {
|
|
207
|
+
dependencies.add(t2);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (t0 === "CALL" && t1 === "FUNCTION") {
|
|
211
|
+
const fn = normalizeToken(tokens[i + 2] ?? "");
|
|
212
|
+
if (isCustomName(fn)) dependencies.add(fn);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if ((t0 === "RAISING" || t0 === "CATCH") && isCustomName(t1)) {
|
|
216
|
+
dependencies.add(t1);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (tokens[i + 1] === "=>" && isCustomName(t0)) {
|
|
220
|
+
dependencies.add(t0);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (t0 === "INTERFACES") for (let j = i + 1; j < tokens.length; j += 1) {
|
|
224
|
+
const next = normalizeToken(tokens[j]);
|
|
225
|
+
if (next === ".") break;
|
|
226
|
+
if (next !== "," && isCustomName(next)) dependencies.add(next);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return Array.from(dependencies).sort((a, b) => a.localeCompare(b));
|
|
230
|
+
}
|
|
231
|
+
function stripInlineComment(line) {
|
|
232
|
+
const idx = line.indexOf("\"");
|
|
233
|
+
return idx >= 0 ? line.slice(0, idx) : line;
|
|
234
|
+
}
|
|
235
|
+
function extractMethodNameFromHeader(line) {
|
|
236
|
+
const trimmed = stripInlineComment(line).trim();
|
|
237
|
+
const upper = trimmed.toUpperCase();
|
|
238
|
+
if (!upper.startsWith("METHOD ") || !upper.endsWith(".")) return;
|
|
239
|
+
const withoutKeyword = trimmed.slice(7, -1).trim();
|
|
240
|
+
if (!withoutKeyword) return;
|
|
241
|
+
return withoutKeyword.split(/\s+/)[0]?.toUpperCase();
|
|
242
|
+
}
|
|
243
|
+
function normalizeMethodBody(source, methodName) {
|
|
244
|
+
const lines = source.split(/\r?\n/);
|
|
245
|
+
if (lines.length === 0) return source;
|
|
246
|
+
const target = methodName.trim().toUpperCase();
|
|
247
|
+
const firstName = extractMethodNameFromHeader(lines[0] ?? "");
|
|
248
|
+
const last = (lines[lines.length - 1] ?? "").trim().toUpperCase();
|
|
249
|
+
if (firstName === target && last === "ENDMETHOD.") return lines.slice(1, -1).join("\n").trimEnd();
|
|
250
|
+
return source.trimEnd();
|
|
251
|
+
}
|
|
252
|
+
function detectMethodBoundary(source, methodName) {
|
|
253
|
+
const lines = source.split(/\r?\n/);
|
|
254
|
+
const target = methodName.trim().toUpperCase();
|
|
255
|
+
const starts = [];
|
|
256
|
+
for (let i = 0; i < lines.length; i += 1) if (extractMethodNameFromHeader(lines[i] ?? "") === target) starts.push(i);
|
|
257
|
+
if (starts.length === 0) return null;
|
|
258
|
+
if (starts.length > 1) return null;
|
|
259
|
+
const startIndex = starts[0] ?? 0;
|
|
260
|
+
for (let i = startIndex + 1; i < lines.length; i += 1) if (stripInlineComment(lines[i] ?? "").trim().toUpperCase() === "ENDMETHOD.") return {
|
|
261
|
+
startLine: startIndex + 1,
|
|
262
|
+
endLine: i + 1
|
|
263
|
+
};
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
export { buildPreset, detectMethodBoundary, extractDependencies, lintAndFix, lintSource, listRules, normalizeMethodBody, stripToPublicApi };
|
|
269
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/preset.ts","../src/lib/lint.ts","../src/lib/context.ts"],"sourcesContent":["import { Config } from '@abaplint/core';\nimport type { LintSystemType } from './types';\n\nfunction cloneDefaultConfig(): Record<string, unknown> {\n return JSON.parse(JSON.stringify(Config.getDefault().get())) as Record<\n string,\n unknown\n >;\n}\n\nexport function buildPreset(\n systemType: LintSystemType,\n): Record<string, unknown> {\n const config: {\n rules?: Record<string, Record<string, unknown>>;\n } = cloneDefaultConfig();\n\n config.rules ??= {};\n\n if (systemType === 'btp') {\n config.rules.cloud_types = {\n ...(config.rules.cloud_types ?? {}),\n severity: 'Error',\n };\n config.rules.strict_sql = {\n ...(config.rules.strict_sql ?? {}),\n severity: 'Error',\n };\n } else {\n delete config.rules.cloud_types;\n config.rules.obsolete_statement = {\n ...(config.rules.obsolete_statement ?? {}),\n severity: 'Warning',\n };\n }\n\n return config;\n}\n","import {\n ArtifactsRules,\n Config,\n Edits,\n MemoryFile,\n Registry,\n Severity,\n} from '@abaplint/core';\nimport { buildPreset } from './preset';\nimport type {\n LintDiagnostic,\n LintOptions,\n LintSeverity,\n RuleInfo,\n} from './types';\n\nconst FALLBACK_FILENAME = 'anonymous.prog.abap';\n\nfunction toSeverity(severity: Severity): LintSeverity {\n switch (severity) {\n case Severity.Warning:\n return 'warning';\n case Severity.Info:\n return 'info';\n default:\n return 'error';\n }\n}\n\nfunction toDiagnostics(registry: Registry): LintDiagnostic[] {\n return registry.findIssues().map((issue) => ({\n key: issue.getKey(),\n message: issue.getMessage(),\n severity: toSeverity(issue.getSeverity()),\n filename: issue.getFilename(),\n start: {\n line: issue.getStart().getRow(),\n column: issue.getStart().getCol(),\n },\n end: {\n line: issue.getEnd().getRow(),\n column: issue.getEnd().getCol(),\n },\n }));\n}\n\nfunction buildConfig(options?: LintOptions): Config {\n const base = buildPreset(options?.systemType ?? 'onpremise') as {\n rules?: Record<string, unknown>;\n };\n\n if (options?.ruleOverrides) {\n base.rules ??= {};\n base.rules = { ...base.rules, ...options.ruleOverrides };\n }\n\n return new Config(JSON.stringify(base));\n}\n\nfunction createRegistry(\n source: string,\n options?: LintOptions,\n): {\n registry: Registry;\n filename: string;\n config: Config;\n} {\n const filename = options?.filename ?? FALLBACK_FILENAME;\n const config = buildConfig(options);\n const registry = new Registry(config);\n registry.addFile(new MemoryFile(filename, source));\n registry.parse();\n\n return { registry, filename, config };\n}\n\nexport function lintSource(\n source: string,\n options?: LintOptions,\n): LintDiagnostic[] {\n const { registry } = createRegistry(source, options);\n return toDiagnostics(registry);\n}\n\nexport function lintAndFix(\n source: string,\n options?: LintOptions,\n): {\n source: string;\n remaining: LintDiagnostic[];\n} {\n const { registry, filename } = createRegistry(source, options);\n const fixes = registry\n .findIssues()\n .map((issue) => issue.getDefaultFix())\n .filter((fix): fix is NonNullable<typeof fix> => Boolean(fix));\n\n if (fixes.length > 0) {\n Edits.applyEditList(registry, fixes);\n registry.parse();\n }\n\n return {\n source: registry.getFileByName(filename)?.getRaw() ?? source,\n remaining: toDiagnostics(registry),\n };\n}\n\nexport function listRules(options?: LintOptions): RuleInfo[] {\n const config = buildConfig(options);\n const enabled = new Set(\n config.getEnabledRules().map((rule) => rule.getMetadata().key),\n );\n\n return ArtifactsRules.getRules()\n .map((rule) => {\n const key = rule.getMetadata().key;\n return {\n key,\n enabled: enabled.has(key),\n config: config.readByRule(key),\n } satisfies RuleInfo;\n })\n .sort((a, b) => a.key.localeCompare(b.key));\n}\n","import type { MethodBoundary, StripResult } from './types';\n\ntype SourceObjectType = 'CLAS' | 'INTF' | 'FUNC' | 'DDLS' | 'PROG' | string;\n\nfunction stripClassSource(source: string): StripResult {\n const lines = source.split(/\\r?\\n/);\n const output: string[] = [];\n\n let inDefinition = false;\n let inHiddenSection = false;\n let foundDefinition = false;\n\n for (const line of lines) {\n const upper = line.trim().toUpperCase();\n\n if (!inDefinition) {\n if (\n upper.startsWith('CLASS ') &&\n upper.includes(' DEFINITION') &&\n upper.endsWith('.')\n ) {\n inDefinition = true;\n foundDefinition = true;\n output.push(line);\n }\n continue;\n }\n\n if (upper === 'PROTECTED SECTION.' || upper === 'PRIVATE SECTION.') {\n inHiddenSection = true;\n continue;\n }\n\n if (upper === 'PUBLIC SECTION.') {\n inHiddenSection = false;\n output.push(line);\n continue;\n }\n\n if (upper === 'ENDCLASS.') {\n output.push(line);\n break;\n }\n\n if (!inHiddenSection) {\n output.push(line);\n }\n }\n\n if (!foundDefinition || output.length === 0) {\n return { source, fallback: true };\n }\n\n return { source: `${output.join('\\n').trim()}\\n`, fallback: false };\n}\n\nfunction stripFunctionSource(source: string): StripResult {\n const lines = source.split(/\\r?\\n/);\n const start = lines.findIndex((line) => /^\\s*FUNCTION\\s+/i.test(line));\n const end = lines.findIndex(\n (line, index) => index >= start && /^\\s*ENDFUNCTION\\./i.test(line),\n );\n\n if (start < 0 || end < 0) {\n return { source, fallback: true };\n }\n\n const kept = lines.slice(start, end + 1).filter((line) => {\n const trimmed = line.trim().toUpperCase();\n return (\n trimmed.startsWith('FUNCTION ') ||\n trimmed.startsWith('IMPORTING') ||\n trimmed.startsWith('EXPORTING') ||\n trimmed.startsWith('CHANGING') ||\n trimmed.startsWith('TABLES') ||\n trimmed.startsWith('EXCEPTIONS') ||\n trimmed.startsWith('ENDFUNCTION.') ||\n trimmed === ''\n );\n });\n\n return { source: `${kept.join('\\n').trim()}\\n`, fallback: false };\n}\n\nexport function stripToPublicApi(\n source: string,\n objectType: SourceObjectType,\n): StripResult {\n const normalized = objectType.toUpperCase();\n\n if (normalized === 'INTF' || normalized === 'DDLS' || normalized === 'PROG') {\n return { source, fallback: false };\n }\n\n if (normalized === 'CLAS') {\n return stripClassSource(source);\n }\n\n if (normalized === 'FUNC') {\n return stripFunctionSource(source);\n }\n\n return { source, fallback: true };\n}\n\nfunction normalizeToken(token: string): string {\n let t = token;\n while (t.startsWith(\"'\")) t = t.slice(1);\n while (t.endsWith(\"'\")) t = t.slice(0, -1);\n return t.toUpperCase();\n}\n\nfunction isCustomName(token: string | undefined): token is string {\n return Boolean(token && /^[ZY][A-Z0-9_]*$/i.test(token));\n}\n\nexport function extractDependencies(source: string): string[] {\n const dependencies = new Set<string>();\n const sanitized = source\n .split(/\\r?\\n/)\n .map((line) => {\n const commentIndex = line.indexOf('\"');\n return commentIndex >= 0 ? line.slice(0, commentIndex) : line;\n })\n .join('\\n');\n const tokens = sanitized.match(/=>|[A-Z_][A-Z0-9_]*|'[^']*'|[.,]/gi) ?? [];\n\n for (let i = 0; i < tokens.length; i += 1) {\n const t0 = normalizeToken(tokens[i]);\n const t1 = normalizeToken(tokens[i + 1] ?? '');\n const t2 = normalizeToken(tokens[i + 2] ?? '');\n const t3 = normalizeToken(tokens[i + 3] ?? '');\n\n if (t0 === 'TYPE' && t1 === 'REF' && t2 === 'TO' && isCustomName(t3)) {\n dependencies.add(t3);\n continue;\n }\n\n if ((t0 === 'NEW' || t0 === 'CAST') && isCustomName(t1)) {\n dependencies.add(t1);\n continue;\n }\n\n if (t0 === 'INHERITING' && t1 === 'FROM' && isCustomName(t2)) {\n dependencies.add(t2);\n continue;\n }\n\n if (t0 === 'CALL' && t1 === 'FUNCTION') {\n const fn = normalizeToken(tokens[i + 2] ?? '');\n if (isCustomName(fn)) {\n dependencies.add(fn);\n }\n continue;\n }\n\n if ((t0 === 'RAISING' || t0 === 'CATCH') && isCustomName(t1)) {\n dependencies.add(t1);\n continue;\n }\n\n if (tokens[i + 1] === '=>' && isCustomName(t0)) {\n dependencies.add(t0);\n continue;\n }\n\n if (t0 === 'INTERFACES') {\n for (let j = i + 1; j < tokens.length; j += 1) {\n const next = normalizeToken(tokens[j]);\n if (next === '.') {\n break;\n }\n if (next !== ',' && isCustomName(next)) {\n dependencies.add(next);\n }\n }\n }\n }\n\n return Array.from(dependencies).sort((a, b) => a.localeCompare(b));\n}\n\nfunction stripInlineComment(line: string): string {\n const idx = line.indexOf('\"');\n return idx >= 0 ? line.slice(0, idx) : line;\n}\n\nfunction extractMethodNameFromHeader(line: string): string | undefined {\n const trimmed = stripInlineComment(line).trim();\n const upper = trimmed.toUpperCase();\n if (!upper.startsWith('METHOD ') || !upper.endsWith('.')) {\n return undefined;\n }\n\n const withoutKeyword = trimmed.slice('METHOD '.length, -1).trim();\n if (!withoutKeyword) {\n return undefined;\n }\n\n return withoutKeyword.split(/\\s+/)[0]?.toUpperCase();\n}\n\nexport function normalizeMethodBody(\n source: string,\n methodName: string,\n): string {\n const lines = source.split(/\\r?\\n/);\n if (lines.length === 0) return source;\n\n const target = methodName.trim().toUpperCase();\n const firstName = extractMethodNameFromHeader(lines[0] ?? '');\n const last = (lines[lines.length - 1] ?? '').trim().toUpperCase();\n\n if (firstName === target && last === 'ENDMETHOD.') {\n return lines.slice(1, -1).join('\\n').trimEnd();\n }\n\n return source.trimEnd();\n}\n\nexport function detectMethodBoundary(\n source: string,\n methodName: string,\n): MethodBoundary | null {\n const lines = source.split(/\\r?\\n/);\n const target = methodName.trim().toUpperCase();\n\n const starts: number[] = [];\n for (let i = 0; i < lines.length; i += 1) {\n if (extractMethodNameFromHeader(lines[i] ?? '') === target) {\n starts.push(i);\n }\n }\n\n if (starts.length === 0) {\n return null;\n }\n\n if (starts.length > 1) {\n return null;\n }\n\n const startIndex = starts[0] ?? 0;\n for (let i = startIndex + 1; i < lines.length; i += 1) {\n if (\n stripInlineComment(lines[i] ?? '')\n .trim()\n .toUpperCase() === 'ENDMETHOD.'\n ) {\n return {\n startLine: startIndex + 1,\n endLine: i + 1,\n };\n }\n }\n\n return null;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +1,31 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@abapify/adt-lint",
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "Shared ABAP lint and source-shaping utilities built on @abaplint/core",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@abaplint/core": "^2.119.15"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/abapify/adt-cli.git",
|
|
25
|
+
"directory": "packages/adt-lint"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/abapify/adt-cli/tree/main/packages/adt-lint#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/abapify/adt-cli/issues"
|
|
30
|
+
}
|
|
31
|
+
}
|