hana-linter 1.0.1 → 1.0.2
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/CHANGELOG.md +9 -0
- package/README.md +23 -23
- package/dist/content-lint.js +2 -27
- package/dist/parsers/hdbfunction/__tests__/extractFunctionParameters.test.js +352 -0
- package/dist/parsers/hdbfunction/index.js +42 -0
- package/dist/parsers/hdbfunction/lexer.js +200 -0
- package/dist/parsers/hdbfunction/parser.js +365 -0
- package/dist/parsers/hdbfunction/visitor.js +105 -0
- package/docs/chevrotain-hdbfunction-parser/prd.md +270 -0
- package/docs/chevrotain-hdbfunction-parser/spec.md +980 -0
- package/package.json +1 -1
- package/src/content-lint.ts +2 -35
- package/src/parsers/hdbfunction/__tests__/extractFunctionParameters.test.ts +399 -0
- package/src/parsers/hdbfunction/index.ts +44 -0
- package/src/parsers/hdbfunction/lexer.ts +217 -0
- package/src/parsers/hdbfunction/parser.ts +456 -0
- package/src/parsers/hdbfunction/visitor.ts +118 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
### 1.0.2
|
|
8
|
+
|
|
9
|
+
- Added Chevrotain-based `.hdbfunction` parser; content-linting of function input parameters is now fully reliable
|
|
10
|
+
- The `.hdbfunction` parser isolates the parameter list from the `RETURNS` clause and the function body (`AS BEGIN … END`), eliminating false positives caused by `IN` keywords inside SQL body statements and column names in `RETURNS TABLE (...)` definitions
|
|
11
|
+
- HANA functions accept only `IN` parameters; the new parser correctly produces only `inputParameter` subjects and never emits `outputParameter` entries for `.hdbfunction` files
|
|
12
|
+
- Removed the shared ad-hoc regex extractor (`extractProcedureFunctionParameters`) that was previously used for both `.hdbprocedure` and `.hdbfunction` files
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
7
16
|
### 1.0.0
|
|
8
17
|
|
|
9
18
|
- Replaced ad-hoc, line-by-line regex extraction with Chevrotain-based lexer + CST parsers for `.hdbtable`, `.hdbprocedure`, and `.hdbview` files
|
package/README.md
CHANGED
|
@@ -46,23 +46,22 @@ Content-based linting uses [Chevrotain](https://chevrotain.io)-powered lexers an
|
|
|
46
46
|
|
|
47
47
|
> **Work in progress.** Not all artifact types have been migrated to the Chevrotain-based parsing infrastructure yet.
|
|
48
48
|
|
|
49
|
-
| Artifact extension | Content extractor | Status
|
|
50
|
-
| ------------------------- | ---------------------- |
|
|
51
|
-
| `.hdbtable` | Chevrotain lexer + CST | ✅ Migrated
|
|
52
|
-
| `.hdbview` | Chevrotain lexer + CST | ✅ Migrated
|
|
53
|
-
| `.hdbprocedure` | Chevrotain lexer + CST | ✅ Migrated
|
|
54
|
-
| `.hdbfunction` |
|
|
55
|
-
| `.hdbtabletype` | — | ❌ Not implemented |
|
|
56
|
-
| `.hdbcalculationview` | — | ❌ Not implemented |
|
|
57
|
-
| `.hdbanalyticalprivilege` | — | ❌ Not implemented |
|
|
58
|
-
| `.hdbrole` | — | ❌ Not implemented |
|
|
59
|
-
| `.hdbsequence` | — | ❌ Not implemented |
|
|
60
|
-
| `.hdbconstraint` | — | ❌ Not implemented |
|
|
61
|
-
| `.hdbschedulerjob` | — | ❌ Not implemented |
|
|
62
|
-
| `.hdbindex` | — | ❌ Not implemented |
|
|
63
|
-
| `.hdbtrigger` | — | ❌ Not implemented |
|
|
64
|
-
|
|
65
|
-
- **Needs migration**: uses an ad-hoc regex scan, subject to false positives from body keywords, multi-line definitions, and block comments.
|
|
49
|
+
| Artifact extension | Content extractor | Status |
|
|
50
|
+
| ------------------------- | ---------------------- | ---------------------- |
|
|
51
|
+
| `.hdbtable` | Chevrotain lexer + CST | ✅ Migrated |
|
|
52
|
+
| `.hdbview` | Chevrotain lexer + CST | ✅ Migrated |
|
|
53
|
+
| `.hdbprocedure` | Chevrotain lexer + CST | ✅ Migrated |
|
|
54
|
+
| `.hdbfunction` | Chevrotain lexer + CST | ✅ Migrated |
|
|
55
|
+
| `.hdbtabletype` | — | ❌ Not yet implemented |
|
|
56
|
+
| `.hdbcalculationview` | — | ❌ Not yet implemented |
|
|
57
|
+
| `.hdbanalyticalprivilege` | — | ❌ Not yet implemented |
|
|
58
|
+
| `.hdbrole` | — | ❌ Not yet implemented |
|
|
59
|
+
| `.hdbsequence` | — | ❌ Not yet implemented |
|
|
60
|
+
| `.hdbconstraint` | — | ❌ Not yet implemented |
|
|
61
|
+
| `.hdbschedulerjob` | — | ❌ Not yet implemented |
|
|
62
|
+
| `.hdbindex` | — | ❌ Not yet implemented |
|
|
63
|
+
| `.hdbtrigger` | — | ❌ Not yet implemented |
|
|
64
|
+
|
|
66
65
|
- **Not implemented**: `contentRuleSets` targeting these extensions will silently return no results — no identifiers are extracted and no content issues are raised.
|
|
67
66
|
|
|
68
67
|
## Install
|
|
@@ -178,12 +177,13 @@ Each `contentRuleSets` item contains:
|
|
|
178
177
|
|
|
179
178
|
Supported extractors in this version:
|
|
180
179
|
|
|
181
|
-
| `target` | Supported extensions
|
|
182
|
-
| ----------------- |
|
|
183
|
-
| `field` | `.hdbtable`
|
|
184
|
-
| `field` | `.hdbview`
|
|
185
|
-
| `inputParameter` | `.hdbprocedure
|
|
186
|
-
| `
|
|
180
|
+
| `target` | Supported extensions | Extracted identifiers |
|
|
181
|
+
| ----------------- | -------------------- | ---------------------------------------------- |
|
|
182
|
+
| `field` | `.hdbtable` | Column names |
|
|
183
|
+
| `field` | `.hdbview` | Column aliases (explicit list or `AS` aliases) |
|
|
184
|
+
| `inputParameter` | `.hdbprocedure` | `IN` and `INOUT` parameters |
|
|
185
|
+
| `inputParameter` | `.hdbfunction` | `IN` parameters (functions accept `IN` only) |
|
|
186
|
+
| `outputParameter` | `.hdbprocedure` | `OUT` and `INOUT` parameters |
|
|
187
187
|
|
|
188
188
|
### Default Config Example
|
|
189
189
|
|
package/dist/content-lint.js
CHANGED
|
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const index_1 = require("./parsers/hdbtable/index");
|
|
10
10
|
const index_2 = require("./parsers/hdbview/index");
|
|
11
11
|
const index_3 = require("./parsers/hdbprocedure/index");
|
|
12
|
+
const index_4 = require("./parsers/hdbfunction/index");
|
|
12
13
|
/**
|
|
13
14
|
* Run content-based naming lint for a file.
|
|
14
15
|
*
|
|
@@ -55,36 +56,10 @@ function extractSubjects(extension, fileContent) {
|
|
|
55
56
|
return (0, index_3.extractProcedureParameters)(fileContent);
|
|
56
57
|
}
|
|
57
58
|
if (extension === '.hdbfunction') {
|
|
58
|
-
return
|
|
59
|
+
return (0, index_4.extractFunctionParameters)(fileContent);
|
|
59
60
|
}
|
|
60
61
|
return [];
|
|
61
62
|
}
|
|
62
|
-
function extractProcedureFunctionParameters(fileContent) {
|
|
63
|
-
const subjects = [];
|
|
64
|
-
const seen = new Set();
|
|
65
|
-
const parameterRegex = /\b(IN|OUT|INOUT)\s+("?[A-Za-z_][A-Za-z0-9_]*"?)\s+[A-Za-z]/gi;
|
|
66
|
-
let match = parameterRegex.exec(fileContent);
|
|
67
|
-
while (match) {
|
|
68
|
-
const mode = match[1];
|
|
69
|
-
const rawName = match[2];
|
|
70
|
-
if (!mode || !rawName) {
|
|
71
|
-
match = parameterRegex.exec(fileContent);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const normalizedMode = mode.toUpperCase();
|
|
75
|
-
const normalizedName = rawName.replace(/^"|"$/g, '');
|
|
76
|
-
if ((normalizedMode === 'IN' || normalizedMode === 'INOUT') && !seen.has(`input:${normalizedName}`)) {
|
|
77
|
-
seen.add(`input:${normalizedName}`);
|
|
78
|
-
subjects.push({ type: 'inputParameter', name: normalizedName });
|
|
79
|
-
}
|
|
80
|
-
if ((normalizedMode === 'OUT' || normalizedMode === 'INOUT') && !seen.has(`output:${normalizedName}`)) {
|
|
81
|
-
seen.add(`output:${normalizedName}`);
|
|
82
|
-
subjects.push({ type: 'outputParameter', name: normalizedName });
|
|
83
|
-
}
|
|
84
|
-
match = parameterRegex.exec(fileContent);
|
|
85
|
-
}
|
|
86
|
-
return subjects;
|
|
87
|
-
}
|
|
88
63
|
function evaluateAllRules(filePath, extension, subject, rules) {
|
|
89
64
|
const issues = [];
|
|
90
65
|
for (const rule of rules) {
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function inputs(ddl) {
|
|
9
|
+
return (0, index_1.extractFunctionParameters)(ddl)
|
|
10
|
+
.filter((s) => s.type === 'inputParameter')
|
|
11
|
+
.map((s) => s.name);
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// AC-1 IN parameter extraction (scalar function)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
(0, vitest_1.describe)('AC-1: IN parameter extraction (scalar function)', () => {
|
|
17
|
+
(0, vitest_1.it)('extracts IN parameters as inputParameter subjects', () => {
|
|
18
|
+
const ddl = `
|
|
19
|
+
FUNCTION MY_FUNC (
|
|
20
|
+
IN IV_CUSTOMER_ID NVARCHAR(10),
|
|
21
|
+
IN IV_DATE DATE
|
|
22
|
+
) RETURNS NVARCHAR(100) AS BEGIN END
|
|
23
|
+
`;
|
|
24
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([
|
|
25
|
+
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID' },
|
|
26
|
+
{ type: 'inputParameter', name: 'IV_DATE' }
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)('does not produce outputParameter entries for any parameter', () => {
|
|
30
|
+
const ddl = `FUNCTION F (IN IV_AMOUNT DECIMAL(18,2)) RETURNS INTEGER AS BEGIN END`;
|
|
31
|
+
const outputSubjects = (0, index_1.extractFunctionParameters)(ddl).filter((s) => s.type === 'outputParameter');
|
|
32
|
+
(0, vitest_1.expect)(outputSubjects).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)('all results carry type "inputParameter"', () => {
|
|
35
|
+
const ddl = `FUNCTION F (IN IV_A INTEGER, IN IV_B NVARCHAR(10)) RETURNS INTEGER AS BEGIN END`;
|
|
36
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl).every((r) => r.type === 'inputParameter')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// AC-2 IN parameter extraction (table function)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
(0, vitest_1.describe)('AC-2: IN parameter extraction (table function)', () => {
|
|
43
|
+
(0, vitest_1.it)('extracts IN params and ignores RETURNS TABLE columns', () => {
|
|
44
|
+
const ddl = `
|
|
45
|
+
FUNCTION MY_FUNC (
|
|
46
|
+
IN IV_STATUS NVARCHAR(1)
|
|
47
|
+
) RETURNS TABLE (ID INTEGER, NAME NVARCHAR(100)) AS BEGIN END
|
|
48
|
+
`;
|
|
49
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([{ type: 'inputParameter', name: 'IV_STATUS' }]);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('does not produce outputParameter entries for a table function', () => {
|
|
52
|
+
const ddl = `
|
|
53
|
+
FUNCTION F (IN IV_X INTEGER)
|
|
54
|
+
RETURNS TABLE (COL1 NVARCHAR(10))
|
|
55
|
+
AS BEGIN END
|
|
56
|
+
`;
|
|
57
|
+
const outputSubjects = (0, index_1.extractFunctionParameters)(ddl).filter((s) => s.type === 'outputParameter');
|
|
58
|
+
(0, vitest_1.expect)(outputSubjects).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// AC-3 RETURNS TABLE columns are not extracted
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
(0, vitest_1.describe)('AC-3: RETURNS TABLE columns not extracted', () => {
|
|
65
|
+
(0, vitest_1.it)('does not extract OUT_COL or IN_COL from RETURNS TABLE definition', () => {
|
|
66
|
+
const ddl = `
|
|
67
|
+
FUNCTION F ()
|
|
68
|
+
RETURNS TABLE (OUT_COL INTEGER, IN_COL NVARCHAR(10))
|
|
69
|
+
AS BEGIN END
|
|
70
|
+
`;
|
|
71
|
+
const names = (0, index_1.extractFunctionParameters)(ddl).map((s) => s.name);
|
|
72
|
+
(0, vitest_1.expect)(names).not.toContain('OUT_COL');
|
|
73
|
+
(0, vitest_1.expect)(names).not.toContain('IN_COL');
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('does not extract column names from a multi-column RETURNS TABLE', () => {
|
|
76
|
+
const ddl = `
|
|
77
|
+
FUNCTION F (IN IV_X INTEGER)
|
|
78
|
+
RETURNS TABLE (
|
|
79
|
+
COL_A BIGINT,
|
|
80
|
+
COL_B NVARCHAR(200),
|
|
81
|
+
COL_C TIMESTAMP
|
|
82
|
+
) AS BEGIN END
|
|
83
|
+
`;
|
|
84
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_X']);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// AC-4 No outputParameter subjects produced
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
(0, vitest_1.describe)('AC-4: no outputParameter subjects produced', () => {
|
|
91
|
+
(0, vitest_1.it)('never emits an outputParameter entry for any parameter', () => {
|
|
92
|
+
const ddl = `
|
|
93
|
+
FUNCTION F (IN IV_A INTEGER, IN IV_B NVARCHAR(10))
|
|
94
|
+
RETURNS INTEGER AS BEGIN END
|
|
95
|
+
`;
|
|
96
|
+
const outputSubjects = (0, index_1.extractFunctionParameters)(ddl).filter((s) => s.type === 'outputParameter');
|
|
97
|
+
(0, vitest_1.expect)(outputSubjects).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)('never emits outputParameter even for a function with no parameters', () => {
|
|
100
|
+
const ddl = `FUNCTION F () RETURNS INTEGER AS BEGIN END`;
|
|
101
|
+
const outputSubjects = (0, index_1.extractFunctionParameters)(ddl).filter((s) => s.type === 'outputParameter');
|
|
102
|
+
(0, vitest_1.expect)(outputSubjects).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// AC-5 TABLE-type IN parameter: inner columns not extracted
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
(0, vitest_1.describe)('AC-5: TABLE-type IN parameter columns not extracted', () => {
|
|
109
|
+
(0, vitest_1.it)('extracts only the outer parameter name, not inner column names', () => {
|
|
110
|
+
const ddl = `
|
|
111
|
+
FUNCTION F (
|
|
112
|
+
IN TV_INPUT TABLE (COL1 INTEGER, COL2 NVARCHAR(100))
|
|
113
|
+
) RETURNS INTEGER AS BEGIN END
|
|
114
|
+
`;
|
|
115
|
+
const result = (0, index_1.extractFunctionParameters)(ddl);
|
|
116
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT' });
|
|
117
|
+
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL1');
|
|
118
|
+
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL2');
|
|
119
|
+
});
|
|
120
|
+
(0, vitest_1.it)('handles multi-column TABLE-type IN param without extracting column names', () => {
|
|
121
|
+
const ddl = `
|
|
122
|
+
FUNCTION F (
|
|
123
|
+
IN TV_DATA TABLE (
|
|
124
|
+
ID BIGINT,
|
|
125
|
+
NAME NVARCHAR(200),
|
|
126
|
+
CREATED_AT TIMESTAMP
|
|
127
|
+
)
|
|
128
|
+
) RETURNS INTEGER AS BEGIN END
|
|
129
|
+
`;
|
|
130
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['TV_DATA']);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// AC-6 Function body SQL does not pollute extraction
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
(0, vitest_1.describe)('AC-6: function body SQL does not pollute extraction', () => {
|
|
137
|
+
(0, vitest_1.it)('ignores IN keyword inside WHERE clause in the body', () => {
|
|
138
|
+
const ddl = `
|
|
139
|
+
FUNCTION F (IN IV_ID INTEGER) RETURNS INTEGER AS
|
|
140
|
+
BEGIN
|
|
141
|
+
SELECT COUNT(*) FROM MY_TABLE WHERE STATUS IN ('A', 'B');
|
|
142
|
+
END
|
|
143
|
+
`;
|
|
144
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_ID']);
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('handles nested BEGIN/END in body without false extraction', () => {
|
|
147
|
+
const ddl = `
|
|
148
|
+
FUNCTION F (IN IV_FLAG BOOLEAN) RETURNS NVARCHAR(10) AS
|
|
149
|
+
BEGIN
|
|
150
|
+
DECLARE result NVARCHAR(10);
|
|
151
|
+
IF IV_FLAG = TRUE THEN
|
|
152
|
+
BEGIN
|
|
153
|
+
result = 'YES';
|
|
154
|
+
END;
|
|
155
|
+
RETURN result;
|
|
156
|
+
END
|
|
157
|
+
`;
|
|
158
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_FLAG']);
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.it)('body content containing IN and function-like syntax is ignored', () => {
|
|
161
|
+
const ddl = `
|
|
162
|
+
FUNCTION F (IN IV_STATUS NVARCHAR(1), IN IV_ID INTEGER)
|
|
163
|
+
RETURNS TABLE (OUT_COL NVARCHAR(10))
|
|
164
|
+
AS
|
|
165
|
+
BEGIN
|
|
166
|
+
RETURN SELECT STATUS FROM T WHERE ID IN (1, 2, 3) AND TYPE = 'IN';
|
|
167
|
+
END
|
|
168
|
+
`;
|
|
169
|
+
const result = (0, index_1.extractFunctionParameters)(ddl);
|
|
170
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
171
|
+
{ type: 'inputParameter', name: 'IV_STATUS' },
|
|
172
|
+
{ type: 'inputParameter', name: 'IV_ID' }
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// AC-7 Block comment exclusion
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
(0, vitest_1.describe)('AC-7: block comment exclusion', () => {
|
|
180
|
+
(0, vitest_1.it)('does not extract a parameter wrapped in /* … */', () => {
|
|
181
|
+
const ddl = `
|
|
182
|
+
FUNCTION F (
|
|
183
|
+
IN IV_ACTIVE BOOLEAN
|
|
184
|
+
/* , IN IV_OLD NVARCHAR(10) */
|
|
185
|
+
) RETURNS INTEGER AS BEGIN END
|
|
186
|
+
`;
|
|
187
|
+
const names = inputs(ddl);
|
|
188
|
+
(0, vitest_1.expect)(names).not.toContain('IV_OLD');
|
|
189
|
+
(0, vitest_1.expect)(names).toContain('IV_ACTIVE');
|
|
190
|
+
});
|
|
191
|
+
(0, vitest_1.it)('handles block comment spanning multiple lines', () => {
|
|
192
|
+
const ddl = `
|
|
193
|
+
FUNCTION F (
|
|
194
|
+
/*
|
|
195
|
+
* IN IV_COMMENTED INTEGER,
|
|
196
|
+
*/
|
|
197
|
+
IN IV_REAL NVARCHAR(10)
|
|
198
|
+
) RETURNS INTEGER AS BEGIN END
|
|
199
|
+
`;
|
|
200
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_REAL']);
|
|
201
|
+
(0, vitest_1.expect)(inputs(ddl)).not.toContain('IV_COMMENTED');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// AC-8 Line comment exclusion
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
(0, vitest_1.describe)('AC-8: line comment exclusion', () => {
|
|
208
|
+
(0, vitest_1.it)('does not extract a parameter on a -- comment line', () => {
|
|
209
|
+
const ddl = `
|
|
210
|
+
FUNCTION F (
|
|
211
|
+
IN IV_ID INTEGER
|
|
212
|
+
-- , IN IV_OLD NVARCHAR(10)
|
|
213
|
+
) RETURNS INTEGER AS BEGIN END
|
|
214
|
+
`;
|
|
215
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_ID']);
|
|
216
|
+
(0, vitest_1.expect)(inputs(ddl)).not.toContain('IV_OLD');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// AC-9 Quoted identifier normalisation
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
(0, vitest_1.describe)('AC-9: quoted identifier normalisation', () => {
|
|
223
|
+
(0, vitest_1.it)('strips double-quotes from a quoted parameter name', () => {
|
|
224
|
+
const ddl = `FUNCTION F (IN "IV_CUSTOMER_ID" NVARCHAR(10)) RETURNS INTEGER AS BEGIN END`;
|
|
225
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toContainEqual({
|
|
226
|
+
type: 'inputParameter',
|
|
227
|
+
name: 'IV_CUSTOMER_ID'
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
(0, vitest_1.it)('handles mixed quoted and unquoted parameters', () => {
|
|
231
|
+
const ddl = `
|
|
232
|
+
FUNCTION F (
|
|
233
|
+
IN "IV_QUOTED" NVARCHAR(10),
|
|
234
|
+
IN IV_UNQUOTED INTEGER
|
|
235
|
+
) RETURNS INTEGER AS BEGIN END
|
|
236
|
+
`;
|
|
237
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_QUOTED', 'IV_UNQUOTED']);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// AC-10 Schema-qualified function name
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
(0, vitest_1.describe)('AC-10: schema-qualified function name', () => {
|
|
244
|
+
(0, vitest_1.it)('parses schema-qualified name without error', () => {
|
|
245
|
+
const ddl = `
|
|
246
|
+
FUNCTION "MY_SCHEMA"."MY_FUNCTION" (IN IV_ID INTEGER)
|
|
247
|
+
RETURNS INTEGER AS BEGIN END
|
|
248
|
+
`;
|
|
249
|
+
(0, vitest_1.expect)(() => (0, index_1.extractFunctionParameters)(ddl)).not.toThrow();
|
|
250
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_ID']);
|
|
251
|
+
});
|
|
252
|
+
(0, vitest_1.it)('parses unquoted schema-qualified name without error', () => {
|
|
253
|
+
const ddl = `FUNCTION MY_SCHEMA.MY_FUNC (IN IV_X INTEGER) RETURNS INTEGER AS BEGIN END`;
|
|
254
|
+
(0, vitest_1.expect)(() => (0, index_1.extractFunctionParameters)(ddl)).not.toThrow();
|
|
255
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_X']);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// AC-11 Function options are ignored
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
(0, vitest_1.describe)('AC-11: function option clauses do not affect extraction', () => {
|
|
262
|
+
(0, vitest_1.it)('handles LANGUAGE SQLSCRIPT SQL SECURITY INVOKER', () => {
|
|
263
|
+
const ddl = `
|
|
264
|
+
FUNCTION F (IN IV_ID INTEGER)
|
|
265
|
+
RETURNS INTEGER
|
|
266
|
+
LANGUAGE SQLSCRIPT
|
|
267
|
+
SQL SECURITY INVOKER
|
|
268
|
+
AS BEGIN END
|
|
269
|
+
`;
|
|
270
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_ID']);
|
|
271
|
+
});
|
|
272
|
+
(0, vitest_1.it)('handles SQL SECURITY DEFINER WITH ENCRYPTION', () => {
|
|
273
|
+
const ddl = `
|
|
274
|
+
FUNCTION F (IN IV_X NVARCHAR(10))
|
|
275
|
+
RETURNS NVARCHAR(10)
|
|
276
|
+
SQL SECURITY DEFINER
|
|
277
|
+
WITH ENCRYPTION
|
|
278
|
+
AS BEGIN END
|
|
279
|
+
`;
|
|
280
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_X']);
|
|
281
|
+
});
|
|
282
|
+
(0, vitest_1.it)('handles DEFAULT SCHEMA option', () => {
|
|
283
|
+
const ddl = `
|
|
284
|
+
FUNCTION F (IN IV_X INTEGER)
|
|
285
|
+
RETURNS INTEGER
|
|
286
|
+
DEFAULT SCHEMA MY_SCHEMA
|
|
287
|
+
AS BEGIN END
|
|
288
|
+
`;
|
|
289
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_X']);
|
|
290
|
+
});
|
|
291
|
+
(0, vitest_1.it)('handles all options combined', () => {
|
|
292
|
+
const ddl = `
|
|
293
|
+
CREATE FUNCTION F (IN IV_A INTEGER, IN IV_B NVARCHAR(10))
|
|
294
|
+
RETURNS TABLE (RES_COL NVARCHAR(10))
|
|
295
|
+
LANGUAGE SQLSCRIPT
|
|
296
|
+
SQL SECURITY DEFINER
|
|
297
|
+
DEFAULT SCHEMA MY_SCHEMA
|
|
298
|
+
WITH ENCRYPTION
|
|
299
|
+
AS BEGIN END
|
|
300
|
+
`;
|
|
301
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_A', 'IV_B']);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// AC-12 Empty parameter list
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
(0, vitest_1.describe)('AC-12: empty parameter list', () => {
|
|
308
|
+
(0, vitest_1.it)('returns empty array for a function with no parameters', () => {
|
|
309
|
+
const ddl = `FUNCTION F () RETURNS INTEGER AS BEGIN END`;
|
|
310
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
(0, vitest_1.it)('returns empty array for a table function with no parameters', () => {
|
|
313
|
+
const ddl = `FUNCTION F () RETURNS TABLE (COL1 INTEGER) AS BEGIN END`;
|
|
314
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// AC-13 CREATE keyword optional
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
(0, vitest_1.describe)('AC-13: CREATE keyword optional', () => {
|
|
321
|
+
(0, vitest_1.it)('extracts identically with and without CREATE keyword', () => {
|
|
322
|
+
const withCreate = `CREATE FUNCTION F (IN IV_ID INTEGER) RETURNS INTEGER AS BEGIN END`;
|
|
323
|
+
const withoutCreate = `FUNCTION F (IN IV_ID INTEGER) RETURNS INTEGER AS BEGIN END`;
|
|
324
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(withCreate)).toEqual((0, index_1.extractFunctionParameters)(withoutCreate));
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// AC-14 Graceful error on unparseable file
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
(0, vitest_1.describe)('AC-14: graceful error handling', () => {
|
|
331
|
+
(0, vitest_1.it)('does not throw on invalid syntax', () => {
|
|
332
|
+
const ddl = `FUNCTION ??? GARBAGE SYNTAX`;
|
|
333
|
+
(0, vitest_1.expect)(() => (0, index_1.extractFunctionParameters)(ddl)).not.toThrow();
|
|
334
|
+
});
|
|
335
|
+
(0, vitest_1.it)('returns an array (possibly empty) on invalid syntax', () => {
|
|
336
|
+
const ddl = `FUNCTION ??? GARBAGE SYNTAX`;
|
|
337
|
+
(0, vitest_1.expect)(Array.isArray((0, index_1.extractFunctionParameters)(ddl))).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
(0, vitest_1.it)('does not throw on an empty string', () => {
|
|
340
|
+
(0, vitest_1.expect)(() => (0, index_1.extractFunctionParameters)('')).not.toThrow();
|
|
341
|
+
(0, vitest_1.expect)(Array.isArray((0, index_1.extractFunctionParameters)(''))).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// AC-15 CRLF line endings
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
(0, vitest_1.describe)('AC-15: CRLF line ending support', () => {
|
|
348
|
+
(0, vitest_1.it)('correctly extracts parameters from a file with CRLF line endings', () => {
|
|
349
|
+
const ddl = 'FUNCTION F (\r\n IN IV_ID INTEGER,\r\n IN IV_NAME NVARCHAR(100)\r\n) RETURNS INTEGER AS BEGIN END';
|
|
350
|
+
(0, vitest_1.expect)(inputs(ddl)).toEqual(['IV_ID', 'IV_NAME']);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractFunctionParameters = extractFunctionParameters;
|
|
4
|
+
const lexer_1 = require("./lexer");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const visitor_1 = require("./visitor");
|
|
7
|
+
/**
|
|
8
|
+
* Extract parameter names from the content of a `.hdbfunction` DDL file.
|
|
9
|
+
*
|
|
10
|
+
* Uses a Chevrotain lexer and CstParser. The parser recognises the function
|
|
11
|
+
* header (parameter list, RETURNS clause, and options) and consumes the entire
|
|
12
|
+
* function body (AS BEGIN … END) as an opaque block, ensuring that SQL keywords
|
|
13
|
+
* inside the body never contaminate the extraction result.
|
|
14
|
+
*
|
|
15
|
+
* HANA functions accept only IN parameters. Every parameter produces an
|
|
16
|
+
* { type: 'inputParameter', name } entry. No 'outputParameter' entries are
|
|
17
|
+
* ever produced. RETURNS clause types and RETURNS TABLE column names are
|
|
18
|
+
* parsed structurally but not extracted.
|
|
19
|
+
*
|
|
20
|
+
* Handles block/line comments, quoted and unquoted identifiers, schema-qualified
|
|
21
|
+
* function names, TABLE-type IN parameters, RETURNS TABLE definitions, and all
|
|
22
|
+
* standard function options. Gracefully returns partial results on invalid input
|
|
23
|
+
* — does not throw.
|
|
24
|
+
*
|
|
25
|
+
* @param fileContent - Raw UTF-8 file content (LF or CRLF).
|
|
26
|
+
* @returns Array of ExtractedSubject with type 'inputParameter' only.
|
|
27
|
+
*/
|
|
28
|
+
function extractFunctionParameters(fileContent) {
|
|
29
|
+
const lexResult = lexer_1.HdbFunctionLexer.tokenize(fileContent);
|
|
30
|
+
// Feed the token stream to the singleton parser.
|
|
31
|
+
parser_1.hdbFunctionParser.input = lexResult.tokens;
|
|
32
|
+
const cst = parser_1.hdbFunctionParser.createFunctionStatement();
|
|
33
|
+
// Lex/parse errors are intentionally not re-thrown — the CST visitor
|
|
34
|
+
// will extract whatever parameters could be parsed from the partial tree.
|
|
35
|
+
// Guard against a completely unrecoverable parse (cst may be undefined).
|
|
36
|
+
if (!cst) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const visitor = new visitor_1.HdbFunctionParameterVisitor();
|
|
40
|
+
visitor.visit(cst);
|
|
41
|
+
return visitor.parameters;
|
|
42
|
+
}
|