hana-linter 0.1.1
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/.github/workflows/npm-publish.yml +29 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +14 -0
- package/README.md +286 -0
- package/dist/cli.js +104 -0
- package/dist/config.js +227 -0
- package/dist/content-lint.js +151 -0
- package/dist/files.js +73 -0
- package/dist/index.js +31 -0
- package/dist/lint.js +195 -0
- package/dist/report.js +28 -0
- package/dist/types/cli.js +2 -0
- package/dist/types/config.js +2 -0
- package/dist/types/issues.js +2 -0
- package/dist/types/rules.js +2 -0
- package/package.json +28 -0
- package/src/assets/.hana-linter.json +224 -0
- package/src/cli.ts +113 -0
- package/src/config.ts +305 -0
- package/src/content-lint.ts +187 -0
- package/src/files.ts +84 -0
- package/src/index.ts +37 -0
- package/src/lint.ts +222 -0
- package/src/report.ts +29 -0
- package/src/types/cli.ts +21 -0
- package/src/types/config.ts +20 -0
- package/src/types/issues.ts +9 -0
- package/src/types/rules.ts +140 -0
- package/tsconfig.json +45 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.lintFileContent = lintFileContent;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/**
|
|
10
|
+
* Run content-based naming lint for a file.
|
|
11
|
+
*
|
|
12
|
+
* @param filePath - Candidate file path.
|
|
13
|
+
* @param contentRuleSets - Configured content rule sets.
|
|
14
|
+
* @returns List of content-based lint issues.
|
|
15
|
+
*/
|
|
16
|
+
async function lintFileContent(filePath, contentRuleSets) {
|
|
17
|
+
const extension = node_path_1.default.extname(filePath);
|
|
18
|
+
const matchingRuleSets = contentRuleSets.filter((ruleSet) => ruleSet.extension === '*' || ruleSet.extension === extension);
|
|
19
|
+
if (matchingRuleSets.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const fileContent = await node_fs_1.promises.readFile(filePath, { encoding: 'utf-8' });
|
|
23
|
+
const extractedSubjects = extractSubjects(extension, fileContent);
|
|
24
|
+
if (extractedSubjects.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const issues = [];
|
|
28
|
+
for (const subject of extractedSubjects) {
|
|
29
|
+
const targetRuleSets = matchingRuleSets.filter((ruleSet) => ruleSet.target === subject.type);
|
|
30
|
+
if (targetRuleSets.length === 0) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const allRules = targetRuleSets.flatMap((ruleSet) => ruleSet.groups.all ?? []);
|
|
34
|
+
const anyRules = targetRuleSets.flatMap((ruleSet) => ruleSet.groups.any ?? []);
|
|
35
|
+
if (allRules.length > 0) {
|
|
36
|
+
issues.push(...evaluateAllRules(filePath, extension, subject, allRules));
|
|
37
|
+
}
|
|
38
|
+
if (anyRules.length > 0) {
|
|
39
|
+
issues.push(...evaluateAnyRules(filePath, extension, subject, anyRules));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
function extractSubjects(extension, fileContent) {
|
|
45
|
+
if (extension === '.hdbtable') {
|
|
46
|
+
return extractTableFields(fileContent);
|
|
47
|
+
}
|
|
48
|
+
if (extension === '.hdbprocedure' || extension === '.hdbfunction') {
|
|
49
|
+
return extractProcedureFunctionParameters(fileContent);
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function extractTableFields(fileContent) {
|
|
54
|
+
const subjects = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const lines = fileContent.split(/\r?\n/);
|
|
57
|
+
const skipKeywords = new Set(['PRIMARY', 'CONSTRAINT', 'UNIQUE', 'FOREIGN', 'CHECK', 'PARTITION', 'INDEX', 'KEY']);
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const trimmedLine = line.trim();
|
|
60
|
+
if (trimmedLine.length === 0 || trimmedLine.startsWith('--')) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const unquotedMatch = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+[A-Za-z]/);
|
|
64
|
+
if (unquotedMatch) {
|
|
65
|
+
const candidate = unquotedMatch[1];
|
|
66
|
+
if (!candidate) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!skipKeywords.has(candidate.toUpperCase()) && !seen.has(candidate)) {
|
|
70
|
+
seen.add(candidate);
|
|
71
|
+
subjects.push({ type: 'field', name: candidate });
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const quotedMatch = trimmedLine.match(/^"([A-Za-z_][A-Za-z0-9_]*)"\s+[A-Za-z]/);
|
|
76
|
+
if (quotedMatch) {
|
|
77
|
+
const candidate = quotedMatch[1];
|
|
78
|
+
if (!candidate) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!seen.has(candidate)) {
|
|
82
|
+
seen.add(candidate);
|
|
83
|
+
subjects.push({ type: 'field', name: candidate });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return subjects;
|
|
88
|
+
}
|
|
89
|
+
function extractProcedureFunctionParameters(fileContent) {
|
|
90
|
+
const subjects = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const parameterRegex = /\b(IN|OUT|INOUT)\s+("?[A-Za-z_][A-Za-z0-9_]*"?)\s+[A-Za-z]/gi;
|
|
93
|
+
let match = parameterRegex.exec(fileContent);
|
|
94
|
+
while (match) {
|
|
95
|
+
const mode = match[1];
|
|
96
|
+
const rawName = match[2];
|
|
97
|
+
if (!mode || !rawName) {
|
|
98
|
+
match = parameterRegex.exec(fileContent);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const normalizedMode = mode.toUpperCase();
|
|
102
|
+
const normalizedName = rawName.replace(/^"|"$/g, '');
|
|
103
|
+
if ((normalizedMode === 'IN' || normalizedMode === 'INOUT') && !seen.has(`input:${normalizedName}`)) {
|
|
104
|
+
seen.add(`input:${normalizedName}`);
|
|
105
|
+
subjects.push({ type: 'inputParameter', name: normalizedName });
|
|
106
|
+
}
|
|
107
|
+
if ((normalizedMode === 'OUT' || normalizedMode === 'INOUT') && !seen.has(`output:${normalizedName}`)) {
|
|
108
|
+
seen.add(`output:${normalizedName}`);
|
|
109
|
+
subjects.push({ type: 'outputParameter', name: normalizedName });
|
|
110
|
+
}
|
|
111
|
+
match = parameterRegex.exec(fileContent);
|
|
112
|
+
}
|
|
113
|
+
return subjects;
|
|
114
|
+
}
|
|
115
|
+
function evaluateAllRules(filePath, extension, subject, rules) {
|
|
116
|
+
const issues = [];
|
|
117
|
+
for (const rule of rules) {
|
|
118
|
+
if (!rule.pattern.test(subject.name)) {
|
|
119
|
+
issues.push({
|
|
120
|
+
filePath,
|
|
121
|
+
artifactName: node_path_1.default.parse(filePath).name,
|
|
122
|
+
extension,
|
|
123
|
+
subjectType: subject.type,
|
|
124
|
+
subjectName: subject.name,
|
|
125
|
+
failedRuleDescription: rule.description,
|
|
126
|
+
failedPattern: toRegexLiteral(rule)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
132
|
+
function evaluateAnyRules(filePath, extension, subject, rules) {
|
|
133
|
+
const hasAtLeastOneMatch = rules.some((rule) => rule.pattern.test(subject.name));
|
|
134
|
+
if (hasAtLeastOneMatch) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
filePath,
|
|
140
|
+
artifactName: node_path_1.default.parse(filePath).name,
|
|
141
|
+
extension,
|
|
142
|
+
subjectType: subject.type,
|
|
143
|
+
subjectName: subject.name,
|
|
144
|
+
failedRuleDescription: 'At least one OR-group rule must match: ' + rules.map((rule) => rule.description).join(' | '),
|
|
145
|
+
failedPattern: rules.map(toRegexLiteral).join(' OR ')
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
function toRegexLiteral(rule) {
|
|
150
|
+
return `/${rule.source}/${rule.flags}`;
|
|
151
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveFilesToValidate = resolveFilesToValidate;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/**
|
|
10
|
+
* Resolve files to validate:
|
|
11
|
+
* - If CLI files are provided: use only existing files from that list.
|
|
12
|
+
* - If none are provided: run full scan from rootDir.
|
|
13
|
+
*
|
|
14
|
+
* @param config - Lint configuration.
|
|
15
|
+
* @param cliInput - Parsed CLI input.
|
|
16
|
+
* @returns List of file paths to validate.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveFilesToValidate(config, cliInput) {
|
|
19
|
+
if (cliInput.files.length > 0) {
|
|
20
|
+
const existingChecks = await Promise.all(cliInput.files.map(async (filePath) => ({
|
|
21
|
+
filePath,
|
|
22
|
+
exists: await isExistingFile(filePath)
|
|
23
|
+
})));
|
|
24
|
+
return existingChecks.filter((item) => item.exists).map((item) => node_path_1.default.normalize(item.filePath));
|
|
25
|
+
}
|
|
26
|
+
const rootExists = await node_fs_1.promises
|
|
27
|
+
.access(config.rootDir)
|
|
28
|
+
.then(() => true)
|
|
29
|
+
.catch(() => false);
|
|
30
|
+
if (!rootExists) {
|
|
31
|
+
throw new Error(`Configured root directory does not exist: "${config.rootDir}"`);
|
|
32
|
+
}
|
|
33
|
+
return collectFiles(config.rootDir, config.ignoredDirectories);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a path exists and is a regular file.
|
|
37
|
+
*
|
|
38
|
+
* @param filePath - Path to validate.
|
|
39
|
+
* @returns True if path exists and is a file.
|
|
40
|
+
*/
|
|
41
|
+
async function isExistingFile(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = await node_fs_1.promises.stat(filePath);
|
|
44
|
+
return stat.isFile();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Recursively collect all files from a directory while skipping ignored folders.
|
|
52
|
+
*
|
|
53
|
+
* @param directoryPath - Absolute or relative folder path.
|
|
54
|
+
* @param ignoredDirectories - Directory names to skip.
|
|
55
|
+
* @returns List of full file paths.
|
|
56
|
+
*/
|
|
57
|
+
async function collectFiles(directoryPath, ignoredDirectories) {
|
|
58
|
+
const entries = await node_fs_1.promises.readdir(directoryPath, { withFileTypes: true });
|
|
59
|
+
const collected = await Promise.all(entries.map(async (entry) => {
|
|
60
|
+
const fullPath = node_path_1.default.join(directoryPath, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
if (ignoredDirectories.includes(entry.name)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
return collectFiles(fullPath, ignoredDirectories);
|
|
66
|
+
}
|
|
67
|
+
if (entry.isFile()) {
|
|
68
|
+
return [fullPath];
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}));
|
|
72
|
+
return collected.flat();
|
|
73
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const cli_1 = require("./cli");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const files_1 = require("./files");
|
|
7
|
+
const lint_1 = require("./lint");
|
|
8
|
+
const report_1 = require("./report");
|
|
9
|
+
/**
|
|
10
|
+
* Application entry point.
|
|
11
|
+
*/
|
|
12
|
+
async function main() {
|
|
13
|
+
const cliInput = (0, cli_1.parseCliInput)();
|
|
14
|
+
if (cliInput.command === 'init') {
|
|
15
|
+
await (0, cli_1.runInit)(cliInput.force);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const jsonConfig = await (0, config_1.readJsonConfig)(cliInput.configPath);
|
|
19
|
+
const config = (0, config_1.toLintConfig)(jsonConfig);
|
|
20
|
+
const filesToValidate = await (0, files_1.resolveFilesToValidate)(config, cliInput);
|
|
21
|
+
const issues = await (0, lint_1.runLint)(config, filesToValidate);
|
|
22
|
+
(0, report_1.printReport)(issues, filesToValidate);
|
|
23
|
+
if (issues.length > 0) {
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
void main().catch((error) => {
|
|
28
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
29
|
+
console.error(`❌ HANA naming lint crashed: ${message}`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
});
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runLint = runLint;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const content_lint_1 = require("./content-lint");
|
|
9
|
+
/**
|
|
10
|
+
* Run naming lint and return all discovered issues.
|
|
11
|
+
*
|
|
12
|
+
* @param config - Lint configuration.
|
|
13
|
+
* @param filesToValidate - File paths to validate.
|
|
14
|
+
* @returns All naming violations.
|
|
15
|
+
*/
|
|
16
|
+
async function runLint(config, filesToValidate) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
for (const filePath of filesToValidate) {
|
|
19
|
+
const fileIssues = validateFileName(filePath, config.extensionRuleSets, config.rootDir);
|
|
20
|
+
issues.push(...fileIssues);
|
|
21
|
+
if (config.contentRuleSets.length > 0) {
|
|
22
|
+
const contentIssues = await (0, content_lint_1.lintFileContent)(filePath, config.contentRuleSets);
|
|
23
|
+
issues.push(...contentIssues);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return issues;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate a single file against configured rule groups for its extension.
|
|
30
|
+
*
|
|
31
|
+
* @param filePath - Full file path.
|
|
32
|
+
* @param extensionRuleSets - Configured extension rule sets.
|
|
33
|
+
* @param rootDir - Configured root directory.
|
|
34
|
+
* @returns List of lint issues.
|
|
35
|
+
*/
|
|
36
|
+
function validateFileName(filePath, extensionRuleSets, rootDir) {
|
|
37
|
+
const extension = node_path_1.default.extname(filePath);
|
|
38
|
+
const matchedRuleSets = findRuleSetsForExtension(extension, extensionRuleSets);
|
|
39
|
+
const ruleSet = mergeRuleSets(extension, matchedRuleSets);
|
|
40
|
+
if (!ruleSet) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const artifactName = getArtifactNameFromFilePath(filePath);
|
|
44
|
+
const issues = [];
|
|
45
|
+
if (ruleSet.folderName && !isFileInRequiredFolder(filePath, rootDir, ruleSet.folderName)) {
|
|
46
|
+
issues.push({
|
|
47
|
+
filePath,
|
|
48
|
+
artifactName,
|
|
49
|
+
extension,
|
|
50
|
+
subjectType: 'artifact',
|
|
51
|
+
subjectName: artifactName,
|
|
52
|
+
failedRuleDescription: `File must be located in folder "${ruleSet.folderName}" under configured rootDir`,
|
|
53
|
+
failedPattern: `folder:${ruleSet.folderName}`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const allRules = ruleSet.groups.all ?? [];
|
|
57
|
+
const anyRules = ruleSet.groups.any ?? [];
|
|
58
|
+
if (allRules.length > 0) {
|
|
59
|
+
issues.push(...evaluateAllRules(filePath, artifactName, extension, allRules));
|
|
60
|
+
}
|
|
61
|
+
if (anyRules.length > 0) {
|
|
62
|
+
issues.push(...evaluateAnyRules(filePath, artifactName, extension, anyRules));
|
|
63
|
+
}
|
|
64
|
+
return issues;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract artifact name from file path.
|
|
68
|
+
*
|
|
69
|
+
* @param filePath - Full file path.
|
|
70
|
+
* @returns Artifact name without extension.
|
|
71
|
+
*/
|
|
72
|
+
function getArtifactNameFromFilePath(filePath) {
|
|
73
|
+
const parsed = node_path_1.default.parse(filePath);
|
|
74
|
+
return parsed.name;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve matching rule sets for a file extension.
|
|
78
|
+
*
|
|
79
|
+
* @param extension - File extension.
|
|
80
|
+
* @param extensionRuleSets - Configured extension rule sets.
|
|
81
|
+
* @returns Matching extension rule sets.
|
|
82
|
+
*/
|
|
83
|
+
function findRuleSetsForExtension(extension, extensionRuleSets) {
|
|
84
|
+
return extensionRuleSets.filter((ruleSet) => ruleSet.extension === '*' || ruleSet.extension === extension);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Merge multiple rule sets into a single effective set.
|
|
88
|
+
*
|
|
89
|
+
* @param extension - File extension.
|
|
90
|
+
* @param ruleSets - Matching rule sets for extension.
|
|
91
|
+
* @returns Merged rule set or undefined when no rules apply.
|
|
92
|
+
*/
|
|
93
|
+
function mergeRuleSets(extension, ruleSets) {
|
|
94
|
+
if (ruleSets.length === 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const specificRuleSets = ruleSets.filter((ruleSet) => ruleSet.extension === extension);
|
|
98
|
+
const wildcardRuleSets = ruleSets.filter((ruleSet) => ruleSet.extension === '*');
|
|
99
|
+
const specificFolderName = specificRuleSets.find((ruleSet) => ruleSet.folderName !== undefined)?.folderName;
|
|
100
|
+
const wildcardFolderName = wildcardRuleSets.find((ruleSet) => ruleSet.folderName !== undefined)?.folderName;
|
|
101
|
+
const allRules = ruleSets.flatMap((ruleSet) => ruleSet.groups.all ?? []);
|
|
102
|
+
const anyRules = ruleSets.flatMap((ruleSet) => ruleSet.groups.any ?? []);
|
|
103
|
+
return {
|
|
104
|
+
extension,
|
|
105
|
+
folderName: specificFolderName ?? wildcardFolderName,
|
|
106
|
+
groups: {
|
|
107
|
+
all: allRules.length > 0 ? allRules : undefined,
|
|
108
|
+
any: anyRules.length > 0 ? anyRules : undefined
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check whether a file is located under a target folder name within rootDir.
|
|
114
|
+
*
|
|
115
|
+
* @param filePath - Candidate file path.
|
|
116
|
+
* @param rootDir - Configured root directory.
|
|
117
|
+
* @param requiredFolderName - Folder name to enforce.
|
|
118
|
+
* @returns True when file is under rootDir and contained in required folder.
|
|
119
|
+
*/
|
|
120
|
+
function isFileInRequiredFolder(filePath, rootDir, requiredFolderName) {
|
|
121
|
+
const absoluteRootDir = node_path_1.default.resolve(rootDir);
|
|
122
|
+
const absoluteFilePath = node_path_1.default.resolve(filePath);
|
|
123
|
+
const relativeToRoot = node_path_1.default.relative(absoluteRootDir, absoluteFilePath);
|
|
124
|
+
// Files outside rootDir are never valid for folder enforcement.
|
|
125
|
+
if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const parentDirectory = node_path_1.default.dirname(relativeToRoot);
|
|
129
|
+
if (parentDirectory === '.' || parentDirectory.length === 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const folderSegments = parentDirectory.split(node_path_1.default.sep);
|
|
133
|
+
return folderSegments.includes(requiredFolderName);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convert a rule to displayable regex literal text.
|
|
137
|
+
*
|
|
138
|
+
* @param rule - Runtime rule.
|
|
139
|
+
* @returns Regex literal-like string.
|
|
140
|
+
*/
|
|
141
|
+
function toRegexLiteral(rule) {
|
|
142
|
+
return `/${rule.source}/${rule.flags}`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build lint issues for failed AND-rules.
|
|
146
|
+
*
|
|
147
|
+
* @param filePath - File path.
|
|
148
|
+
* @param artifactName - Artifact name.
|
|
149
|
+
* @param extension - Artifact extension.
|
|
150
|
+
* @param rules - AND-rules list.
|
|
151
|
+
* @returns Lint issues for failed rules.
|
|
152
|
+
*/
|
|
153
|
+
function evaluateAllRules(filePath, artifactName, extension, rules) {
|
|
154
|
+
const issues = [];
|
|
155
|
+
for (const rule of rules) {
|
|
156
|
+
if (!rule.pattern.test(artifactName)) {
|
|
157
|
+
issues.push({
|
|
158
|
+
filePath,
|
|
159
|
+
artifactName,
|
|
160
|
+
extension,
|
|
161
|
+
subjectType: 'artifact',
|
|
162
|
+
subjectName: artifactName,
|
|
163
|
+
failedRuleDescription: rule.description,
|
|
164
|
+
failedPattern: toRegexLiteral(rule)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return issues;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build lint issue for failed OR-group.
|
|
172
|
+
*
|
|
173
|
+
* @param filePath - File path.
|
|
174
|
+
* @param artifactName - Artifact name.
|
|
175
|
+
* @param extension - Artifact extension.
|
|
176
|
+
* @param rules - OR-rules list.
|
|
177
|
+
* @returns Single lint issue if OR-group fails; otherwise empty list.
|
|
178
|
+
*/
|
|
179
|
+
function evaluateAnyRules(filePath, artifactName, extension, rules) {
|
|
180
|
+
const hasAtLeastOneMatch = rules.some((rule) => rule.pattern.test(artifactName));
|
|
181
|
+
if (hasAtLeastOneMatch) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
filePath,
|
|
187
|
+
artifactName,
|
|
188
|
+
extension,
|
|
189
|
+
subjectType: 'artifact',
|
|
190
|
+
subjectName: artifactName,
|
|
191
|
+
failedRuleDescription: 'At least one OR-group rule must match: ' + rules.map((rule) => rule.description).join(' | '),
|
|
192
|
+
failedPattern: rules.map(toRegexLiteral).join(' OR ')
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printReport = printReport;
|
|
4
|
+
/**
|
|
5
|
+
* Print lint result report to stdout/stderr.
|
|
6
|
+
*
|
|
7
|
+
* @param issues - Found naming violations.
|
|
8
|
+
* @param filesToValidate - Number of candidate files processed.
|
|
9
|
+
*/
|
|
10
|
+
function printReport(issues, filesToValidate) {
|
|
11
|
+
if (issues.length === 0) {
|
|
12
|
+
console.info(`✅ HANA naming lint passed. Checked ${filesToValidate.length} file(s).`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.error(`❌ HANA naming lint failed. Violations: ${issues.length}`);
|
|
16
|
+
console.error('');
|
|
17
|
+
for (const issue of issues) {
|
|
18
|
+
console.error(`- File: ${issue.filePath}`);
|
|
19
|
+
console.error(` Artifact: ${issue.artifactName}`);
|
|
20
|
+
if (issue.subjectType && issue.subjectName) {
|
|
21
|
+
console.error(` Subject: ${issue.subjectType} (${issue.subjectName})`);
|
|
22
|
+
}
|
|
23
|
+
console.error(` Type: ${issue.extension}`);
|
|
24
|
+
console.error(` Failed rule: ${issue.failedRuleDescription}`);
|
|
25
|
+
console.error(` Expected regex: ${issue.failedPattern}`);
|
|
26
|
+
console.error('');
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hana-linter",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Linter for SAP HANA artifacts in an SAP CAP project",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hana-linter": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"hana",
|
|
11
|
+
"lint",
|
|
12
|
+
"cap",
|
|
13
|
+
"npm"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/qualiture/hana-linter.git"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "echo \"Warning: no test specified\" && exit 0"
|
|
22
|
+
},
|
|
23
|
+
"author": "Robin van het Hof (Qualiture)",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|