hana-linter 1.0.2 → 1.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/CHANGELOG.md +14 -0
- package/dist/assets/.hana-linter.json +224 -0
- package/dist/content-lint.js +2 -0
- package/dist/parsers/hdbfunction/__tests__/extractFunctionParameters.test.js +8 -7
- package/dist/parsers/hdbfunction/visitor.js +5 -4
- package/dist/parsers/hdbprocedure/__tests__/extractProcedureParameters.test.js +17 -15
- package/dist/parsers/hdbprocedure/visitor.js +6 -5
- package/dist/parsers/hdbtable/__tests__/extractTableColumns.test.js +6 -6
- package/dist/parsers/hdbtable/visitor.js +3 -2
- package/dist/parsers/hdbview/__tests__/extractViewColumns.test.js +4 -4
- package/dist/parsers/hdbview/visitor.js +1 -1
- package/dist/report.js +38 -7
- package/package.json +3 -2
- package/src/content-lint.ts +2 -0
- package/src/parsers/hdbfunction/__tests__/extractFunctionParameters.test.ts +8 -7
- package/src/parsers/hdbfunction/visitor.ts +6 -5
- package/src/parsers/hdbprocedure/__tests__/extractProcedureParameters.test.ts +17 -15
- package/src/parsers/hdbprocedure/visitor.ts +7 -6
- package/src/parsers/hdbtable/__tests__/extractTableColumns.test.ts +6 -6
- package/src/parsers/hdbtable/visitor.ts +3 -2
- package/src/parsers/hdbview/__tests__/extractViewColumns.test.ts +4 -4
- package/src/parsers/hdbview/visitor.ts +1 -1
- package/src/report.ts +40 -7
- package/src/types/issues.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
### 1.1.0
|
|
8
|
+
|
|
9
|
+
- Console output is now grouped by file, then by failed rule, reducing noise when multiple violations exist in the same file
|
|
10
|
+
- Added line numbers to content-lint violations (fields, input/output parameters) so users can jump directly to the offending identifier in their editor
|
|
11
|
+
- All four Chevrotain visitors (`hdbtable`, `hdbview`, `hdbprocedure`, `hdbfunction`) now capture `token.startLine` from the parsed token and propagate it through `ExtractedSubject` and `LintIssue`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### 1.0.3
|
|
16
|
+
|
|
17
|
+
- Fixed build: `src/assets/.hana-linter.json` is now correctly copied to `dist/assets/` during `pnpm build` via `copyfiles`, ensuring the default config template is bundled in the published package
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
7
21
|
### 1.0.2
|
|
8
22
|
|
|
9
23
|
- Added Chevrotain-based `.hdbfunction` parser; content-linting of function input parameters is now fully reliable
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rootDir": "db",
|
|
3
|
+
"ignoredDirectories": ["node_modules", ".git", "gen"],
|
|
4
|
+
"extensionRuleSets": [
|
|
5
|
+
{
|
|
6
|
+
"extension": "hdb*",
|
|
7
|
+
"groups": {
|
|
8
|
+
"all": [
|
|
9
|
+
{
|
|
10
|
+
"description": "Upper snake case only",
|
|
11
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"description": "Max length 30",
|
|
15
|
+
"pattern": "^.{1,30}$",
|
|
16
|
+
"flags": "u"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"extension": ".hdbtable",
|
|
23
|
+
"folderName": "tables",
|
|
24
|
+
"groups": {
|
|
25
|
+
"any": [
|
|
26
|
+
{
|
|
27
|
+
"description": "Prefix T_",
|
|
28
|
+
"pattern": "^T_.+"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"description": "Prefix TX_",
|
|
32
|
+
"pattern": "^TX_.+"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"extension": ".hdbview",
|
|
39
|
+
"folderName": "sqlviews",
|
|
40
|
+
"groups": {
|
|
41
|
+
"all": [
|
|
42
|
+
{
|
|
43
|
+
"description": "Prefix V_",
|
|
44
|
+
"pattern": "^V_.+"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"extension": ".hdbfunction",
|
|
51
|
+
"folderName": "functions",
|
|
52
|
+
"groups": {
|
|
53
|
+
"any": [
|
|
54
|
+
{
|
|
55
|
+
"description": "Prefix TF_",
|
|
56
|
+
"pattern": "^TF_.+"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"description": "Prefix SF_",
|
|
60
|
+
"pattern": "^SF_.+"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"extension": ".hdbprocedure",
|
|
67
|
+
"folderName": "procedures",
|
|
68
|
+
"groups": {
|
|
69
|
+
"all": [
|
|
70
|
+
{
|
|
71
|
+
"description": "Prefix PR_",
|
|
72
|
+
"pattern": "^PR_.+"
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"extension": ".hdbcalculationview",
|
|
79
|
+
"folderName": "models",
|
|
80
|
+
"groups": {
|
|
81
|
+
"all": [
|
|
82
|
+
{
|
|
83
|
+
"description": "Prefix CV_",
|
|
84
|
+
"pattern": "^CV_.+"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"extension": ".hdbanalyticalprivilege",
|
|
91
|
+
"folderName": "authorizations",
|
|
92
|
+
"groups": {
|
|
93
|
+
"all": [
|
|
94
|
+
{
|
|
95
|
+
"description": "Prefix AP_",
|
|
96
|
+
"pattern": "^AP_.+"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"extension": ".hdbrole",
|
|
103
|
+
"folderName": "roles",
|
|
104
|
+
"groups": {
|
|
105
|
+
"all": [
|
|
106
|
+
{
|
|
107
|
+
"description": "Prefix R_",
|
|
108
|
+
"pattern": "^R_.+"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"extension": ".hdbsequence",
|
|
115
|
+
"folderName": "sequences",
|
|
116
|
+
"groups": {
|
|
117
|
+
"all": [
|
|
118
|
+
{
|
|
119
|
+
"description": "Prefix S_",
|
|
120
|
+
"pattern": "^S_.+"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"extension": ".hdbtabletype",
|
|
127
|
+
"folderName": "tabletypes",
|
|
128
|
+
"groups": {
|
|
129
|
+
"all": [
|
|
130
|
+
{
|
|
131
|
+
"description": "Prefix TT_",
|
|
132
|
+
"pattern": "^TT_.+"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"extension": ".hdbconstraint",
|
|
139
|
+
"folderName": "constraints",
|
|
140
|
+
"groups": {
|
|
141
|
+
"all": [
|
|
142
|
+
{
|
|
143
|
+
"description": "Prefix C_",
|
|
144
|
+
"pattern": "^C_.+"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"extension": ".hdbschedulerjob",
|
|
151
|
+
"folderName": "jobs",
|
|
152
|
+
"groups": {
|
|
153
|
+
"all": [
|
|
154
|
+
{
|
|
155
|
+
"description": "Prefix J_",
|
|
156
|
+
"pattern": "^J_.+"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"extension": ".hdbindex",
|
|
163
|
+
"folderName": "indexes",
|
|
164
|
+
"groups": {
|
|
165
|
+
"all": [
|
|
166
|
+
{
|
|
167
|
+
"description": "Prefix IDX_",
|
|
168
|
+
"pattern": "^IDX_.+"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"extension": ".hdbtrigger",
|
|
175
|
+
"folderName": "triggers",
|
|
176
|
+
"groups": {
|
|
177
|
+
"all": [
|
|
178
|
+
{
|
|
179
|
+
"description": "Prefix TR_",
|
|
180
|
+
"pattern": "^TR_.+"
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"contentRuleSets": [
|
|
187
|
+
{
|
|
188
|
+
"extension": ".hdbtable",
|
|
189
|
+
"target": "field",
|
|
190
|
+
"groups": {
|
|
191
|
+
"all": [
|
|
192
|
+
{
|
|
193
|
+
"description": "Field names in uppercase snake case",
|
|
194
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"extension": ".hdbprocedure",
|
|
201
|
+
"target": "inputParameter",
|
|
202
|
+
"groups": {
|
|
203
|
+
"all": [
|
|
204
|
+
{
|
|
205
|
+
"description": "Input parameters prefixed with IP_",
|
|
206
|
+
"pattern": "^IP_[A-Z0-9_]+$"
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"extension": ".hdbprocedure",
|
|
213
|
+
"target": "outputParameter",
|
|
214
|
+
"groups": {
|
|
215
|
+
"all": [
|
|
216
|
+
{
|
|
217
|
+
"description": "Output parameters prefixed with OP_",
|
|
218
|
+
"pattern": "^OP_[A-Z0-9_]+$"
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
package/dist/content-lint.js
CHANGED
|
@@ -70,6 +70,7 @@ function evaluateAllRules(filePath, extension, subject, rules) {
|
|
|
70
70
|
extension,
|
|
71
71
|
subjectType: subject.type,
|
|
72
72
|
subjectName: subject.name,
|
|
73
|
+
lineNumber: subject.lineNumber,
|
|
73
74
|
failedRuleDescription: rule.description,
|
|
74
75
|
failedPattern: toRegexLiteral(rule)
|
|
75
76
|
});
|
|
@@ -89,6 +90,7 @@ function evaluateAnyRules(filePath, extension, subject, rules) {
|
|
|
89
90
|
extension,
|
|
90
91
|
subjectType: subject.type,
|
|
91
92
|
subjectName: subject.name,
|
|
93
|
+
lineNumber: subject.lineNumber,
|
|
92
94
|
failedRuleDescription: 'At least one OR-group rule must match: ' + rules.map((rule) => rule.description).join(' | '),
|
|
93
95
|
failedPattern: rules.map(toRegexLiteral).join(' OR ')
|
|
94
96
|
}
|
|
@@ -22,8 +22,8 @@ function inputs(ddl) {
|
|
|
22
22
|
) RETURNS NVARCHAR(100) AS BEGIN END
|
|
23
23
|
`;
|
|
24
24
|
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([
|
|
25
|
-
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID' },
|
|
26
|
-
{ type: 'inputParameter', name: 'IV_DATE' }
|
|
25
|
+
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID', lineNumber: 3 },
|
|
26
|
+
{ type: 'inputParameter', name: 'IV_DATE', lineNumber: 4 }
|
|
27
27
|
]);
|
|
28
28
|
});
|
|
29
29
|
(0, vitest_1.it)('does not produce outputParameter entries for any parameter', () => {
|
|
@@ -46,7 +46,7 @@ function inputs(ddl) {
|
|
|
46
46
|
IN IV_STATUS NVARCHAR(1)
|
|
47
47
|
) RETURNS TABLE (ID INTEGER, NAME NVARCHAR(100)) AS BEGIN END
|
|
48
48
|
`;
|
|
49
|
-
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([{ type: 'inputParameter', name: 'IV_STATUS' }]);
|
|
49
|
+
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toEqual([{ type: 'inputParameter', name: 'IV_STATUS', lineNumber: 3 }]);
|
|
50
50
|
});
|
|
51
51
|
(0, vitest_1.it)('does not produce outputParameter entries for a table function', () => {
|
|
52
52
|
const ddl = `
|
|
@@ -113,7 +113,7 @@ function inputs(ddl) {
|
|
|
113
113
|
) RETURNS INTEGER AS BEGIN END
|
|
114
114
|
`;
|
|
115
115
|
const result = (0, index_1.extractFunctionParameters)(ddl);
|
|
116
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT' });
|
|
116
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT', lineNumber: 3 });
|
|
117
117
|
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL1');
|
|
118
118
|
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL2');
|
|
119
119
|
});
|
|
@@ -168,8 +168,8 @@ function inputs(ddl) {
|
|
|
168
168
|
`;
|
|
169
169
|
const result = (0, index_1.extractFunctionParameters)(ddl);
|
|
170
170
|
(0, vitest_1.expect)(result).toEqual([
|
|
171
|
-
{ type: 'inputParameter', name: 'IV_STATUS' },
|
|
172
|
-
{ type: 'inputParameter', name: 'IV_ID' }
|
|
171
|
+
{ type: 'inputParameter', name: 'IV_STATUS', lineNumber: 2 },
|
|
172
|
+
{ type: 'inputParameter', name: 'IV_ID', lineNumber: 2 }
|
|
173
173
|
]);
|
|
174
174
|
});
|
|
175
175
|
});
|
|
@@ -224,7 +224,8 @@ function inputs(ddl) {
|
|
|
224
224
|
const ddl = `FUNCTION F (IN "IV_CUSTOMER_ID" NVARCHAR(10)) RETURNS INTEGER AS BEGIN END`;
|
|
225
225
|
(0, vitest_1.expect)((0, index_1.extractFunctionParameters)(ddl)).toContainEqual({
|
|
226
226
|
type: 'inputParameter',
|
|
227
|
-
name: 'IV_CUSTOMER_ID'
|
|
227
|
+
name: 'IV_CUSTOMER_ID',
|
|
228
|
+
lineNumber: 1
|
|
228
229
|
});
|
|
229
230
|
});
|
|
230
231
|
(0, vitest_1.it)('handles mixed quoted and unquoted parameters', () => {
|
|
@@ -45,10 +45,10 @@ class HdbFunctionParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
45
45
|
const nameNodes = ctx['parameterName'];
|
|
46
46
|
if (!nameNodes?.length || !nameNodes[0])
|
|
47
47
|
return;
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
48
|
+
const extracted = this.extractName(nameNodes[0]);
|
|
49
|
+
if (!extracted)
|
|
50
50
|
return;
|
|
51
|
-
this.parameters.push({ type: 'inputParameter', name });
|
|
51
|
+
this.parameters.push({ type: 'inputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
52
52
|
}
|
|
53
53
|
// -----------------------------------------------------------------------
|
|
54
54
|
// tableColumnDefinition — no-op.
|
|
@@ -99,7 +99,8 @@ class HdbFunctionParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
99
99
|
if (!token)
|
|
100
100
|
return undefined;
|
|
101
101
|
const raw = token.image;
|
|
102
|
-
|
|
102
|
+
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
103
|
+
return { name, lineNumber: token.startLine };
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
exports.HdbFunctionParameterVisitor = HdbFunctionParameterVisitor;
|
|
@@ -27,8 +27,8 @@ function outputs(ddl) {
|
|
|
27
27
|
) AS BEGIN END
|
|
28
28
|
`;
|
|
29
29
|
(0, vitest_1.expect)((0, index_1.extractProcedureParameters)(ddl)).toEqual([
|
|
30
|
-
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID' },
|
|
31
|
-
{ type: 'inputParameter', name: 'IV_DATE' }
|
|
30
|
+
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID', lineNumber: 3 },
|
|
31
|
+
{ type: 'inputParameter', name: 'IV_DATE', lineNumber: 4 }
|
|
32
32
|
]);
|
|
33
33
|
});
|
|
34
34
|
(0, vitest_1.it)('does not produce outputParameter entries for IN parameters', () => {
|
|
@@ -52,8 +52,8 @@ function outputs(ddl) {
|
|
|
52
52
|
) AS BEGIN END
|
|
53
53
|
`;
|
|
54
54
|
(0, vitest_1.expect)((0, index_1.extractProcedureParameters)(ddl)).toEqual([
|
|
55
|
-
{ type: 'outputParameter', name: 'EV_COUNT' },
|
|
56
|
-
{ type: 'outputParameter', name: 'EV_STATUS' }
|
|
55
|
+
{ type: 'outputParameter', name: 'EV_COUNT', lineNumber: 3 },
|
|
56
|
+
{ type: 'outputParameter', name: 'EV_STATUS', lineNumber: 4 }
|
|
57
57
|
]);
|
|
58
58
|
});
|
|
59
59
|
(0, vitest_1.it)('does not produce inputParameter entries for OUT parameters', () => {
|
|
@@ -68,15 +68,15 @@ function outputs(ddl) {
|
|
|
68
68
|
(0, vitest_1.it)('produces both types for an INOUT scalar parameter', () => {
|
|
69
69
|
const ddl = `PROCEDURE P (INOUT CV_STATUS NVARCHAR(1)) AS BEGIN END`;
|
|
70
70
|
(0, vitest_1.expect)((0, index_1.extractProcedureParameters)(ddl)).toEqual([
|
|
71
|
-
{ type: 'inputParameter', name: 'CV_STATUS' },
|
|
72
|
-
{ type: 'outputParameter', name: 'CV_STATUS' }
|
|
71
|
+
{ type: 'inputParameter', name: 'CV_STATUS', lineNumber: 1 },
|
|
72
|
+
{ type: 'outputParameter', name: 'CV_STATUS', lineNumber: 1 }
|
|
73
73
|
]);
|
|
74
74
|
});
|
|
75
75
|
(0, vitest_1.it)('produces both types for an INOUT TABLE parameter', () => {
|
|
76
76
|
const ddl = `PROCEDURE P (INOUT TV_RESULT TABLE (ID INTEGER)) AS BEGIN END`;
|
|
77
77
|
const result = (0, index_1.extractProcedureParameters)(ddl);
|
|
78
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_RESULT' });
|
|
79
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'TV_RESULT' });
|
|
78
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_RESULT', lineNumber: 1 });
|
|
79
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'TV_RESULT', lineNumber: 1 });
|
|
80
80
|
});
|
|
81
81
|
(0, vitest_1.it)('yields exactly two entries for a single INOUT parameter', () => {
|
|
82
82
|
const ddl = `PROCEDURE P (INOUT CV_X INTEGER) AS BEGIN END`;
|
|
@@ -94,7 +94,7 @@ function outputs(ddl) {
|
|
|
94
94
|
) AS BEGIN END
|
|
95
95
|
`;
|
|
96
96
|
const result = (0, index_1.extractProcedureParameters)(ddl);
|
|
97
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT' });
|
|
97
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT', lineNumber: 3 });
|
|
98
98
|
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL1');
|
|
99
99
|
(0, vitest_1.expect)(result.map((s) => s.name)).not.toContain('COL2');
|
|
100
100
|
});
|
|
@@ -226,14 +226,16 @@ function outputs(ddl) {
|
|
|
226
226
|
const ddl = `PROCEDURE P (IN "IV_CUSTOMER_ID" NVARCHAR(10)) AS BEGIN END`;
|
|
227
227
|
(0, vitest_1.expect)((0, index_1.extractProcedureParameters)(ddl)).toContainEqual({
|
|
228
228
|
type: 'inputParameter',
|
|
229
|
-
name: 'IV_CUSTOMER_ID'
|
|
229
|
+
name: 'IV_CUSTOMER_ID',
|
|
230
|
+
lineNumber: 1
|
|
230
231
|
});
|
|
231
232
|
});
|
|
232
233
|
(0, vitest_1.it)('strips double-quotes from a quoted OUT parameter name', () => {
|
|
233
234
|
const ddl = `PROCEDURE P (OUT "EV_RESULT" INTEGER) AS BEGIN END`;
|
|
234
235
|
(0, vitest_1.expect)((0, index_1.extractProcedureParameters)(ddl)).toContainEqual({
|
|
235
236
|
type: 'outputParameter',
|
|
236
|
-
name: 'EV_RESULT'
|
|
237
|
+
name: 'EV_RESULT',
|
|
238
|
+
lineNumber: 1
|
|
237
239
|
});
|
|
238
240
|
});
|
|
239
241
|
});
|
|
@@ -355,10 +357,10 @@ function outputs(ddl) {
|
|
|
355
357
|
) AS BEGIN END
|
|
356
358
|
`;
|
|
357
359
|
const result = (0, index_1.extractProcedureParameters)(ddl);
|
|
358
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'IV_ID' });
|
|
359
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'EV_COUNT' });
|
|
360
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'CV_STATUS' });
|
|
361
|
-
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'CV_STATUS' });
|
|
360
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'IV_ID', lineNumber: 3 });
|
|
361
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'EV_COUNT', lineNumber: 4 });
|
|
362
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'inputParameter', name: 'CV_STATUS', lineNumber: 5 });
|
|
363
|
+
(0, vitest_1.expect)(result).toContainEqual({ type: 'outputParameter', name: 'CV_STATUS', lineNumber: 5 });
|
|
362
364
|
(0, vitest_1.expect)(result).toHaveLength(4);
|
|
363
365
|
});
|
|
364
366
|
(0, vitest_1.it)('handles mixed scalar and TABLE parameters', () => {
|
|
@@ -53,14 +53,14 @@ class HdbProcedureParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
53
53
|
const nameNodes = ctx['parameterName'];
|
|
54
54
|
if (!nameNodes?.length || !nameNodes[0])
|
|
55
55
|
return;
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
56
|
+
const extracted = this.extractName(nameNodes[0]);
|
|
57
|
+
if (!extracted)
|
|
58
58
|
return;
|
|
59
59
|
if (isIn || isInout) {
|
|
60
|
-
this.parameters.push({ type: 'inputParameter', name });
|
|
60
|
+
this.parameters.push({ type: 'inputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
61
61
|
}
|
|
62
62
|
if (isOut || isInout) {
|
|
63
|
-
this.parameters.push({ type: 'outputParameter', name });
|
|
63
|
+
this.parameters.push({ type: 'outputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
// -----------------------------------------------------------------------
|
|
@@ -101,7 +101,8 @@ class HdbProcedureParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
101
101
|
if (!token)
|
|
102
102
|
return undefined;
|
|
103
103
|
const raw = token.image;
|
|
104
|
-
|
|
104
|
+
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
105
|
+
return { name, lineNumber: token.startLine };
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
exports.HdbProcedureParameterVisitor = HdbProcedureParameterVisitor;
|
|
@@ -22,9 +22,9 @@ function names(ddl) {
|
|
|
22
22
|
)
|
|
23
23
|
`;
|
|
24
24
|
(0, vitest_1.expect)((0, index_1.extractTableColumns)(ddl)).toEqual([
|
|
25
|
-
{ type: 'field', name: 'ID' },
|
|
26
|
-
{ type: 'field', name: 'NAME' },
|
|
27
|
-
{ type: 'field', name: 'CREATED_AT' }
|
|
25
|
+
{ type: 'field', name: 'ID', lineNumber: 3 },
|
|
26
|
+
{ type: 'field', name: 'NAME', lineNumber: 4 },
|
|
27
|
+
{ type: 'field', name: 'CREATED_AT', lineNumber: 5 }
|
|
28
28
|
]);
|
|
29
29
|
});
|
|
30
30
|
(0, vitest_1.it)('does not include the constraint name in the result', () => {
|
|
@@ -108,7 +108,7 @@ function names(ddl) {
|
|
|
108
108
|
(0, vitest_1.describe)('AC-4: quoted identifier normalisation', () => {
|
|
109
109
|
(0, vitest_1.it)('strips surrounding double-quotes from a quoted column identifier', () => {
|
|
110
110
|
const ddl = `COLUMN TABLE "MY_TABLE" ("MY_COLUMN" NVARCHAR(100))`;
|
|
111
|
-
(0, vitest_1.expect)((0, index_1.extractTableColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_COLUMN' });
|
|
111
|
+
(0, vitest_1.expect)((0, index_1.extractTableColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_COLUMN', lineNumber: 1 });
|
|
112
112
|
});
|
|
113
113
|
(0, vitest_1.it)('strips quotes even when the table name is also quoted', () => {
|
|
114
114
|
const ddl = `COLUMN TABLE "SCHEMA"."T" ("COL" INTEGER)`;
|
|
@@ -269,8 +269,8 @@ function names(ddl) {
|
|
|
269
269
|
(0, vitest_1.it)('handles a mix in the same table', () => {
|
|
270
270
|
const ddl = `COLUMN TABLE T ("QUOTED" INTEGER, UNQUOTED NVARCHAR(10))`;
|
|
271
271
|
(0, vitest_1.expect)((0, index_1.extractTableColumns)(ddl)).toEqual([
|
|
272
|
-
{ type: 'field', name: 'QUOTED' },
|
|
273
|
-
{ type: 'field', name: 'UNQUOTED' }
|
|
272
|
+
{ type: 'field', name: 'QUOTED', lineNumber: 1 },
|
|
273
|
+
{ type: 'field', name: 'UNQUOTED', lineNumber: 1 }
|
|
274
274
|
]);
|
|
275
275
|
});
|
|
276
276
|
});
|
|
@@ -43,10 +43,11 @@ class HdbTableColumnVisitor extends BaseCstVisitorWithDefaults {
|
|
|
43
43
|
if (!tokenElement) {
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
const
|
|
46
|
+
const token = tokenElement;
|
|
47
|
+
const raw = token.image;
|
|
47
48
|
// Strip surrounding double-quotes from quoted identifiers ("MY_COL" → MY_COL).
|
|
48
49
|
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
49
|
-
this.columns.push({ type: 'field', name });
|
|
50
|
+
this.columns.push({ type: 'field', name, lineNumber: token.startLine });
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
exports.HdbTableColumnVisitor = HdbTableColumnVisitor;
|
|
@@ -41,8 +41,8 @@ function names(ddl) {
|
|
|
41
41
|
(0, vitest_1.it)('extracts AS aliases from the top-level SELECT clause', () => {
|
|
42
42
|
const ddl = `VIEW V_BAR AS SELECT T."CUST_ID" AS "ID", T."CUST_NAME" AS "NAME" FROM T`;
|
|
43
43
|
(0, vitest_1.expect)((0, index_1.extractViewColumns)(ddl)).toEqual([
|
|
44
|
-
{ type: 'field', name: 'ID' },
|
|
45
|
-
{ type: 'field', name: 'NAME' }
|
|
44
|
+
{ type: 'field', name: 'ID', lineNumber: 1 },
|
|
45
|
+
{ type: 'field', name: 'NAME', lineNumber: 1 }
|
|
46
46
|
]);
|
|
47
47
|
});
|
|
48
48
|
(0, vitest_1.it)('handles unquoted alias names', () => {
|
|
@@ -158,11 +158,11 @@ function names(ddl) {
|
|
|
158
158
|
(0, vitest_1.describe)('AC-6: quoted identifier normalisation', () => {
|
|
159
159
|
(0, vitest_1.it)('strips double-quotes from quoted aliases', () => {
|
|
160
160
|
const ddl = `VIEW V AS SELECT T.X AS "MY_ALIAS" FROM T`;
|
|
161
|
-
(0, vitest_1.expect)((0, index_1.extractViewColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_ALIAS' });
|
|
161
|
+
(0, vitest_1.expect)((0, index_1.extractViewColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_ALIAS', lineNumber: 1 });
|
|
162
162
|
});
|
|
163
163
|
(0, vitest_1.it)('strips double-quotes from quoted names in explicit column list', () => {
|
|
164
164
|
const ddl = `VIEW V ("MY_COL") AS SELECT T.X FROM T`;
|
|
165
|
-
(0, vitest_1.expect)((0, index_1.extractViewColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_COL' });
|
|
165
|
+
(0, vitest_1.expect)((0, index_1.extractViewColumns)(ddl)).toContainEqual({ type: 'field', name: 'MY_COL', lineNumber: 1 });
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
168
|
// ---------------------------------------------------------------------------
|
|
@@ -113,7 +113,7 @@ class HdbViewColumnVisitor extends BaseCstVisitorWithDefaults {
|
|
|
113
113
|
const raw = token.image;
|
|
114
114
|
// Strip surrounding double-quotes: "MY_COL" → MY_COL.
|
|
115
115
|
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
116
|
-
this.columns.push({ type: 'field', name });
|
|
116
|
+
this.columns.push({ type: 'field', name, lineNumber: token.startLine });
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
exports.HdbViewColumnVisitor = HdbViewColumnVisitor;
|
package/dist/report.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.printReport = printReport;
|
|
4
4
|
/**
|
|
5
5
|
* Print lint result report to stdout/stderr.
|
|
6
|
+
* Issues are grouped by file, then by failed rule.
|
|
6
7
|
*
|
|
7
8
|
* @param issues - Found naming violations.
|
|
8
9
|
* @param filesToValidate - Number of candidate files processed.
|
|
@@ -14,15 +15,45 @@ function printReport(issues, filesToValidate) {
|
|
|
14
15
|
}
|
|
15
16
|
console.error(`❌ HANA naming lint failed. Violations: ${issues.length}`);
|
|
16
17
|
console.error('');
|
|
18
|
+
// Group issues by file path.
|
|
19
|
+
const byFile = new Map();
|
|
17
20
|
for (const issue of issues) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const bucket = byFile.get(issue.filePath);
|
|
22
|
+
if (bucket) {
|
|
23
|
+
bucket.push(issue);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
byFile.set(issue.filePath, [issue]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const [filePath, fileIssues] of byFile) {
|
|
30
|
+
const count = fileIssues.length;
|
|
31
|
+
console.error(`File: ${filePath} (${count} violation${count === 1 ? '' : 's'})`);
|
|
32
|
+
// Group file issues by failed rule description.
|
|
33
|
+
const byRule = new Map();
|
|
34
|
+
for (const issue of fileIssues) {
|
|
35
|
+
const key = `${issue.failedRuleDescription}||${issue.failedPattern}`;
|
|
36
|
+
const bucket = byRule.get(key);
|
|
37
|
+
if (bucket) {
|
|
38
|
+
bucket.push(issue);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
byRule.set(key, [issue]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const ruleIssues of byRule.values()) {
|
|
45
|
+
const { failedRuleDescription, failedPattern } = ruleIssues[0];
|
|
46
|
+
console.error(` Rule: "${failedRuleDescription}" (pattern: ${failedPattern})`);
|
|
47
|
+
for (const issue of ruleIssues) {
|
|
48
|
+
const location = issue.lineNumber !== undefined ? ` at line ${issue.lineNumber}` : '';
|
|
49
|
+
if (issue.subjectType && issue.subjectName) {
|
|
50
|
+
console.error(` - ${issue.subjectType} "${issue.subjectName}"${location}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(` - artifact "${issue.artifactName}"${location}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
22
56
|
}
|
|
23
|
-
console.error(` Type: ${issue.extension}`);
|
|
24
|
-
console.error(` Failed rule: ${issue.failedRuleDescription}`);
|
|
25
|
-
console.error(` Expected regex: ${issue.failedPattern}`);
|
|
26
57
|
console.error('');
|
|
27
58
|
}
|
|
28
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hana-linter",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Linter for SAP HANA artifacts in an SAP CAP project",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "git+https://github.com/qualiture/hana-linter.git"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc",
|
|
20
|
+
"build": "tsc && copyfiles -u 1 \"src/assets/.hana-linter.json\" dist",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:coverage": "vitest run --coverage"
|
|
23
23
|
},
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^25.9.3",
|
|
28
28
|
"@vitest/coverage-v8": "^4.1.9",
|
|
29
|
+
"copyfiles": "^2.4.1",
|
|
29
30
|
"vitest": "^4.1.9"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
package/src/content-lint.ts
CHANGED
|
@@ -83,6 +83,7 @@ function evaluateAllRules(filePath: string, extension: string, subject: Extracte
|
|
|
83
83
|
extension,
|
|
84
84
|
subjectType: subject.type,
|
|
85
85
|
subjectName: subject.name,
|
|
86
|
+
lineNumber: subject.lineNumber,
|
|
86
87
|
failedRuleDescription: rule.description,
|
|
87
88
|
failedPattern: toRegexLiteral(rule)
|
|
88
89
|
});
|
|
@@ -106,6 +107,7 @@ function evaluateAnyRules(filePath: string, extension: string, subject: Extracte
|
|
|
106
107
|
extension,
|
|
107
108
|
subjectType: subject.type,
|
|
108
109
|
subjectName: subject.name,
|
|
110
|
+
lineNumber: subject.lineNumber,
|
|
109
111
|
failedRuleDescription: 'At least one OR-group rule must match: ' + rules.map((rule) => rule.description).join(' | '),
|
|
110
112
|
failedPattern: rules.map(toRegexLiteral).join(' OR ')
|
|
111
113
|
}
|
|
@@ -24,8 +24,8 @@ describe('AC-1: IN parameter extraction (scalar function)', () => {
|
|
|
24
24
|
) RETURNS NVARCHAR(100) AS BEGIN END
|
|
25
25
|
`;
|
|
26
26
|
expect(extractFunctionParameters(ddl)).toEqual([
|
|
27
|
-
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID' },
|
|
28
|
-
{ type: 'inputParameter', name: 'IV_DATE' }
|
|
27
|
+
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID', lineNumber: 3 },
|
|
28
|
+
{ type: 'inputParameter', name: 'IV_DATE', lineNumber: 4 }
|
|
29
29
|
]);
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -52,7 +52,7 @@ describe('AC-2: IN parameter extraction (table function)', () => {
|
|
|
52
52
|
IN IV_STATUS NVARCHAR(1)
|
|
53
53
|
) RETURNS TABLE (ID INTEGER, NAME NVARCHAR(100)) AS BEGIN END
|
|
54
54
|
`;
|
|
55
|
-
expect(extractFunctionParameters(ddl)).toEqual([{ type: 'inputParameter', name: 'IV_STATUS' }]);
|
|
55
|
+
expect(extractFunctionParameters(ddl)).toEqual([{ type: 'inputParameter', name: 'IV_STATUS', lineNumber: 3 }]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
it('does not produce outputParameter entries for a table function', () => {
|
|
@@ -128,7 +128,7 @@ describe('AC-5: TABLE-type IN parameter columns not extracted', () => {
|
|
|
128
128
|
) RETURNS INTEGER AS BEGIN END
|
|
129
129
|
`;
|
|
130
130
|
const result = extractFunctionParameters(ddl);
|
|
131
|
-
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT' });
|
|
131
|
+
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT', lineNumber: 3 });
|
|
132
132
|
expect(result.map((s) => s.name)).not.toContain('COL1');
|
|
133
133
|
expect(result.map((s) => s.name)).not.toContain('COL2');
|
|
134
134
|
});
|
|
@@ -188,8 +188,8 @@ describe('AC-6: function body SQL does not pollute extraction', () => {
|
|
|
188
188
|
`;
|
|
189
189
|
const result = extractFunctionParameters(ddl);
|
|
190
190
|
expect(result).toEqual([
|
|
191
|
-
{ type: 'inputParameter', name: 'IV_STATUS' },
|
|
192
|
-
{ type: 'inputParameter', name: 'IV_ID' }
|
|
191
|
+
{ type: 'inputParameter', name: 'IV_STATUS', lineNumber: 2 },
|
|
192
|
+
{ type: 'inputParameter', name: 'IV_ID', lineNumber: 2 }
|
|
193
193
|
]);
|
|
194
194
|
});
|
|
195
195
|
});
|
|
@@ -251,7 +251,8 @@ describe('AC-9: quoted identifier normalisation', () => {
|
|
|
251
251
|
const ddl = `FUNCTION F (IN "IV_CUSTOMER_ID" NVARCHAR(10)) RETURNS INTEGER AS BEGIN END`;
|
|
252
252
|
expect(extractFunctionParameters(ddl)).toContainEqual({
|
|
253
253
|
type: 'inputParameter',
|
|
254
|
-
name: 'IV_CUSTOMER_ID'
|
|
254
|
+
name: 'IV_CUSTOMER_ID',
|
|
255
|
+
lineNumber: 1
|
|
255
256
|
});
|
|
256
257
|
});
|
|
257
258
|
|
|
@@ -49,10 +49,10 @@ export class HdbFunctionParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
49
49
|
const nameNodes = ctx['parameterName'] as CstNode[] | undefined;
|
|
50
50
|
if (!nameNodes?.length || !nameNodes[0]) return;
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
52
|
+
const extracted = this.extractName(nameNodes[0]);
|
|
53
|
+
if (!extracted) return;
|
|
54
54
|
|
|
55
|
-
this.parameters.push({ type: 'inputParameter', name });
|
|
55
|
+
this.parameters.push({ type: 'inputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// -----------------------------------------------------------------------
|
|
@@ -97,7 +97,7 @@ export class HdbFunctionParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
97
97
|
// Strips surrounding double-quotes from quoted identifiers.
|
|
98
98
|
// -----------------------------------------------------------------------
|
|
99
99
|
|
|
100
|
-
private extractName(node: CstNode): string | undefined {
|
|
100
|
+
private extractName(node: CstNode): { name: string; lineNumber?: number } | undefined {
|
|
101
101
|
if (!node.children) return undefined;
|
|
102
102
|
|
|
103
103
|
// parameterName → identifier → Identifier | QuotedIdentifier
|
|
@@ -113,6 +113,7 @@ export class HdbFunctionParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
113
113
|
if (!token) return undefined;
|
|
114
114
|
|
|
115
115
|
const raw = token.image;
|
|
116
|
-
|
|
116
|
+
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
117
|
+
return { name, lineNumber: token.startLine };
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -30,8 +30,8 @@ describe('AC-1: IN parameter extraction', () => {
|
|
|
30
30
|
) AS BEGIN END
|
|
31
31
|
`;
|
|
32
32
|
expect(extractProcedureParameters(ddl)).toEqual([
|
|
33
|
-
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID' },
|
|
34
|
-
{ type: 'inputParameter', name: 'IV_DATE' }
|
|
33
|
+
{ type: 'inputParameter', name: 'IV_CUSTOMER_ID', lineNumber: 3 },
|
|
34
|
+
{ type: 'inputParameter', name: 'IV_DATE', lineNumber: 4 }
|
|
35
35
|
]);
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -59,8 +59,8 @@ describe('AC-2: OUT parameter extraction', () => {
|
|
|
59
59
|
) AS BEGIN END
|
|
60
60
|
`;
|
|
61
61
|
expect(extractProcedureParameters(ddl)).toEqual([
|
|
62
|
-
{ type: 'outputParameter', name: 'EV_COUNT' },
|
|
63
|
-
{ type: 'outputParameter', name: 'EV_STATUS' }
|
|
62
|
+
{ type: 'outputParameter', name: 'EV_COUNT', lineNumber: 3 },
|
|
63
|
+
{ type: 'outputParameter', name: 'EV_STATUS', lineNumber: 4 }
|
|
64
64
|
]);
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -78,16 +78,16 @@ describe('AC-3: INOUT parameter yields inputParameter and outputParameter', () =
|
|
|
78
78
|
it('produces both types for an INOUT scalar parameter', () => {
|
|
79
79
|
const ddl = `PROCEDURE P (INOUT CV_STATUS NVARCHAR(1)) AS BEGIN END`;
|
|
80
80
|
expect(extractProcedureParameters(ddl)).toEqual([
|
|
81
|
-
{ type: 'inputParameter', name: 'CV_STATUS' },
|
|
82
|
-
{ type: 'outputParameter', name: 'CV_STATUS' }
|
|
81
|
+
{ type: 'inputParameter', name: 'CV_STATUS', lineNumber: 1 },
|
|
82
|
+
{ type: 'outputParameter', name: 'CV_STATUS', lineNumber: 1 }
|
|
83
83
|
]);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
it('produces both types for an INOUT TABLE parameter', () => {
|
|
87
87
|
const ddl = `PROCEDURE P (INOUT TV_RESULT TABLE (ID INTEGER)) AS BEGIN END`;
|
|
88
88
|
const result = extractProcedureParameters(ddl);
|
|
89
|
-
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_RESULT' });
|
|
90
|
-
expect(result).toContainEqual({ type: 'outputParameter', name: 'TV_RESULT' });
|
|
89
|
+
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_RESULT', lineNumber: 1 });
|
|
90
|
+
expect(result).toContainEqual({ type: 'outputParameter', name: 'TV_RESULT', lineNumber: 1 });
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it('yields exactly two entries for a single INOUT parameter', () => {
|
|
@@ -108,7 +108,7 @@ describe('AC-4: TABLE-type parameter columns not extracted', () => {
|
|
|
108
108
|
) AS BEGIN END
|
|
109
109
|
`;
|
|
110
110
|
const result = extractProcedureParameters(ddl);
|
|
111
|
-
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT' });
|
|
111
|
+
expect(result).toContainEqual({ type: 'inputParameter', name: 'TV_INPUT', lineNumber: 3 });
|
|
112
112
|
expect(result.map((s) => s.name)).not.toContain('COL1');
|
|
113
113
|
expect(result.map((s) => s.name)).not.toContain('COL2');
|
|
114
114
|
});
|
|
@@ -253,7 +253,8 @@ describe('AC-8: quoted identifier normalisation', () => {
|
|
|
253
253
|
const ddl = `PROCEDURE P (IN "IV_CUSTOMER_ID" NVARCHAR(10)) AS BEGIN END`;
|
|
254
254
|
expect(extractProcedureParameters(ddl)).toContainEqual({
|
|
255
255
|
type: 'inputParameter',
|
|
256
|
-
name: 'IV_CUSTOMER_ID'
|
|
256
|
+
name: 'IV_CUSTOMER_ID',
|
|
257
|
+
lineNumber: 1
|
|
257
258
|
});
|
|
258
259
|
});
|
|
259
260
|
|
|
@@ -261,7 +262,8 @@ describe('AC-8: quoted identifier normalisation', () => {
|
|
|
261
262
|
const ddl = `PROCEDURE P (OUT "EV_RESULT" INTEGER) AS BEGIN END`;
|
|
262
263
|
expect(extractProcedureParameters(ddl)).toContainEqual({
|
|
263
264
|
type: 'outputParameter',
|
|
264
|
-
name: 'EV_RESULT'
|
|
265
|
+
name: 'EV_RESULT',
|
|
266
|
+
lineNumber: 1
|
|
265
267
|
});
|
|
266
268
|
});
|
|
267
269
|
});
|
|
@@ -401,10 +403,10 @@ describe('mixed parameter modes', () => {
|
|
|
401
403
|
) AS BEGIN END
|
|
402
404
|
`;
|
|
403
405
|
const result = extractProcedureParameters(ddl);
|
|
404
|
-
expect(result).toContainEqual({ type: 'inputParameter', name: 'IV_ID' });
|
|
405
|
-
expect(result).toContainEqual({ type: 'outputParameter', name: 'EV_COUNT' });
|
|
406
|
-
expect(result).toContainEqual({ type: 'inputParameter', name: 'CV_STATUS' });
|
|
407
|
-
expect(result).toContainEqual({ type: 'outputParameter', name: 'CV_STATUS' });
|
|
406
|
+
expect(result).toContainEqual({ type: 'inputParameter', name: 'IV_ID', lineNumber: 3 });
|
|
407
|
+
expect(result).toContainEqual({ type: 'outputParameter', name: 'EV_COUNT', lineNumber: 4 });
|
|
408
|
+
expect(result).toContainEqual({ type: 'inputParameter', name: 'CV_STATUS', lineNumber: 5 });
|
|
409
|
+
expect(result).toContainEqual({ type: 'outputParameter', name: 'CV_STATUS', lineNumber: 5 });
|
|
408
410
|
expect(result).toHaveLength(4);
|
|
409
411
|
});
|
|
410
412
|
|
|
@@ -58,14 +58,14 @@ export class HdbProcedureParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
58
58
|
const nameNodes = ctx['parameterName'] as CstNode[] | undefined;
|
|
59
59
|
if (!nameNodes?.length || !nameNodes[0]) return;
|
|
60
60
|
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
61
|
+
const extracted = this.extractName(nameNodes[0]);
|
|
62
|
+
if (!extracted) return;
|
|
63
63
|
|
|
64
64
|
if (isIn || isInout) {
|
|
65
|
-
this.parameters.push({ type: 'inputParameter', name });
|
|
65
|
+
this.parameters.push({ type: 'inputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
66
66
|
}
|
|
67
67
|
if (isOut || isInout) {
|
|
68
|
-
this.parameters.push({ type: 'outputParameter', name });
|
|
68
|
+
this.parameters.push({ type: 'outputParameter', name: extracted.name, lineNumber: extracted.lineNumber });
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -98,7 +98,7 @@ export class HdbProcedureParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
98
98
|
// Strips surrounding double-quotes from quoted identifiers.
|
|
99
99
|
// -----------------------------------------------------------------------
|
|
100
100
|
|
|
101
|
-
private extractName(node: CstNode): string | undefined {
|
|
101
|
+
private extractName(node: CstNode): { name: string; lineNumber?: number } | undefined {
|
|
102
102
|
if (!node.children) return undefined;
|
|
103
103
|
|
|
104
104
|
// parameterName → identifier → Identifier | QuotedIdentifier
|
|
@@ -114,6 +114,7 @@ export class HdbProcedureParameterVisitor extends BaseCstVisitorWithDefaults {
|
|
|
114
114
|
if (!token) return undefined;
|
|
115
115
|
|
|
116
116
|
const raw = token.image;
|
|
117
|
-
|
|
117
|
+
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
118
|
+
return { name, lineNumber: token.startLine };
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -24,9 +24,9 @@ describe('AC-1: standard column extraction', () => {
|
|
|
24
24
|
)
|
|
25
25
|
`;
|
|
26
26
|
expect(extractTableColumns(ddl)).toEqual([
|
|
27
|
-
{ type: 'field', name: 'ID' },
|
|
28
|
-
{ type: 'field', name: 'NAME' },
|
|
29
|
-
{ type: 'field', name: 'CREATED_AT' }
|
|
27
|
+
{ type: 'field', name: 'ID', lineNumber: 3 },
|
|
28
|
+
{ type: 'field', name: 'NAME', lineNumber: 4 },
|
|
29
|
+
{ type: 'field', name: 'CREATED_AT', lineNumber: 5 }
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -121,7 +121,7 @@ describe('AC-3: line comment exclusion', () => {
|
|
|
121
121
|
describe('AC-4: quoted identifier normalisation', () => {
|
|
122
122
|
it('strips surrounding double-quotes from a quoted column identifier', () => {
|
|
123
123
|
const ddl = `COLUMN TABLE "MY_TABLE" ("MY_COLUMN" NVARCHAR(100))`;
|
|
124
|
-
expect(extractTableColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_COLUMN' });
|
|
124
|
+
expect(extractTableColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_COLUMN', lineNumber: 1 });
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
it('strips quotes even when the table name is also quoted', () => {
|
|
@@ -309,8 +309,8 @@ describe('mixed quoted and unquoted identifiers', () => {
|
|
|
309
309
|
it('handles a mix in the same table', () => {
|
|
310
310
|
const ddl = `COLUMN TABLE T ("QUOTED" INTEGER, UNQUOTED NVARCHAR(10))`;
|
|
311
311
|
expect(extractTableColumns(ddl)).toEqual([
|
|
312
|
-
{ type: 'field', name: 'QUOTED' },
|
|
313
|
-
{ type: 'field', name: 'UNQUOTED' }
|
|
312
|
+
{ type: 'field', name: 'QUOTED', lineNumber: 1 },
|
|
313
|
+
{ type: 'field', name: 'UNQUOTED', lineNumber: 1 }
|
|
314
314
|
]);
|
|
315
315
|
});
|
|
316
316
|
});
|
|
@@ -50,10 +50,11 @@ export class HdbTableColumnVisitor extends BaseCstVisitorWithDefaults {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const
|
|
53
|
+
const token = tokenElement as IToken;
|
|
54
|
+
const raw = token.image;
|
|
54
55
|
// Strip surrounding double-quotes from quoted identifiers ("MY_COL" → MY_COL).
|
|
55
56
|
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
56
57
|
|
|
57
|
-
this.columns.push({ type: 'field', name });
|
|
58
|
+
this.columns.push({ type: 'field', name, lineNumber: token.startLine });
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -49,8 +49,8 @@ describe('AC-2: SELECT alias extraction (no explicit column list)', () => {
|
|
|
49
49
|
it('extracts AS aliases from the top-level SELECT clause', () => {
|
|
50
50
|
const ddl = `VIEW V_BAR AS SELECT T."CUST_ID" AS "ID", T."CUST_NAME" AS "NAME" FROM T`;
|
|
51
51
|
expect(extractViewColumns(ddl)).toEqual([
|
|
52
|
-
{ type: 'field', name: 'ID' },
|
|
53
|
-
{ type: 'field', name: 'NAME' }
|
|
52
|
+
{ type: 'field', name: 'ID', lineNumber: 1 },
|
|
53
|
+
{ type: 'field', name: 'NAME', lineNumber: 1 }
|
|
54
54
|
]);
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -182,12 +182,12 @@ describe('AC-5: line comment exclusion', () => {
|
|
|
182
182
|
describe('AC-6: quoted identifier normalisation', () => {
|
|
183
183
|
it('strips double-quotes from quoted aliases', () => {
|
|
184
184
|
const ddl = `VIEW V AS SELECT T.X AS "MY_ALIAS" FROM T`;
|
|
185
|
-
expect(extractViewColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_ALIAS' });
|
|
185
|
+
expect(extractViewColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_ALIAS', lineNumber: 1 });
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
it('strips double-quotes from quoted names in explicit column list', () => {
|
|
189
189
|
const ddl = `VIEW V ("MY_COL") AS SELECT T.X FROM T`;
|
|
190
|
-
expect(extractViewColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_COL' });
|
|
190
|
+
expect(extractViewColumns(ddl)).toContainEqual({ type: 'field', name: 'MY_COL', lineNumber: 1 });
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
193
|
|
|
@@ -125,6 +125,6 @@ export class HdbViewColumnVisitor extends BaseCstVisitorWithDefaults {
|
|
|
125
125
|
const raw = token.image;
|
|
126
126
|
// Strip surrounding double-quotes: "MY_COL" → MY_COL.
|
|
127
127
|
const name = raw.startsWith('"') ? raw.slice(1, -1) : raw;
|
|
128
|
-
this.columns.push({ type: 'field', name });
|
|
128
|
+
this.columns.push({ type: 'field', name, lineNumber: token.startLine });
|
|
129
129
|
}
|
|
130
130
|
}
|
package/src/report.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { LintIssue } from './types/issues';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Print lint result report to stdout/stderr.
|
|
5
|
+
* Issues are grouped by file, then by failed rule.
|
|
5
6
|
*
|
|
6
7
|
* @param issues - Found naming violations.
|
|
7
8
|
* @param filesToValidate - Number of candidate files processed.
|
|
@@ -15,15 +16,47 @@ export function printReport(issues: readonly LintIssue[], filesToValidate: reado
|
|
|
15
16
|
console.error(`❌ HANA naming lint failed. Violations: ${issues.length}`);
|
|
16
17
|
console.error('');
|
|
17
18
|
|
|
19
|
+
// Group issues by file path.
|
|
20
|
+
const byFile = new Map<string, LintIssue[]>();
|
|
18
21
|
for (const issue of issues) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const bucket = byFile.get(issue.filePath);
|
|
23
|
+
if (bucket) {
|
|
24
|
+
bucket.push(issue);
|
|
25
|
+
} else {
|
|
26
|
+
byFile.set(issue.filePath, [issue]);
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [filePath, fileIssues] of byFile) {
|
|
31
|
+
const count = fileIssues.length;
|
|
32
|
+
console.error(`File: ${filePath} (${count} violation${count === 1 ? '' : 's'})`);
|
|
33
|
+
|
|
34
|
+
// Group file issues by failed rule description.
|
|
35
|
+
const byRule = new Map<string, LintIssue[]>();
|
|
36
|
+
for (const issue of fileIssues) {
|
|
37
|
+
const key = `${issue.failedRuleDescription}||${issue.failedPattern}`;
|
|
38
|
+
const bucket = byRule.get(key);
|
|
39
|
+
if (bucket) {
|
|
40
|
+
bucket.push(issue);
|
|
41
|
+
} else {
|
|
42
|
+
byRule.set(key, [issue]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const ruleIssues of byRule.values()) {
|
|
47
|
+
const { failedRuleDescription, failedPattern } = ruleIssues[0]!;
|
|
48
|
+
console.error(` Rule: "${failedRuleDescription}" (pattern: ${failedPattern})`);
|
|
49
|
+
|
|
50
|
+
for (const issue of ruleIssues) {
|
|
51
|
+
const location = issue.lineNumber !== undefined ? ` at line ${issue.lineNumber}` : '';
|
|
52
|
+
if (issue.subjectType && issue.subjectName) {
|
|
53
|
+
console.error(` - ${issue.subjectType} "${issue.subjectName}"${location}`);
|
|
54
|
+
} else {
|
|
55
|
+
console.error(` - artifact "${issue.artifactName}"${location}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
console.error('');
|
|
28
61
|
}
|
|
29
62
|
}
|
package/src/types/issues.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ContentTarget } from './rules';
|
|
|
3
3
|
export type ExtractedSubject = {
|
|
4
4
|
readonly type: ContentTarget;
|
|
5
5
|
readonly name: string;
|
|
6
|
+
readonly lineNumber?: number;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export type LintIssue = {
|
|
@@ -11,6 +12,7 @@ export type LintIssue = {
|
|
|
11
12
|
readonly extension: string;
|
|
12
13
|
readonly subjectType?: 'artifact' | 'field' | 'inputParameter' | 'outputParameter';
|
|
13
14
|
readonly subjectName?: string;
|
|
15
|
+
readonly lineNumber?: number;
|
|
14
16
|
readonly failedRuleDescription: string;
|
|
15
17
|
readonly failedPattern: string;
|
|
16
18
|
};
|