@sasjs/cli 6.6.0 → 6.7.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.
Files changed (31) hide show
  1. package/README.md +2 -1
  2. package/build/commands/help/help.js +10 -0
  3. package/build/commands/index.d.ts +1 -0
  4. package/build/commands/index.js +3 -1
  5. package/build/commands/logcheck/collectLogs.d.ts +60 -0
  6. package/build/commands/logcheck/collectLogs.js +386 -0
  7. package/build/commands/logcheck/logCheckConfig.d.ts +72 -0
  8. package/build/commands/logcheck/logCheckConfig.js +283 -0
  9. package/build/commands/logcheck/logcheckCommand.d.ts +6 -0
  10. package/build/commands/logcheck/logcheckCommand.js +200 -0
  11. package/build/commands/logcheck/processLogCheck.d.ts +44 -0
  12. package/build/commands/logcheck/processLogCheck.js +242 -0
  13. package/build/commands/logcheck/renderLogCheck.d.ts +128 -0
  14. package/build/commands/logcheck/renderLogCheck.js +423 -0
  15. package/build/commands/logcheck/spec/collectLogs.spec.d.ts +1 -0
  16. package/build/commands/logcheck/spec/collectLogs.spec.js +477 -0
  17. package/build/commands/logcheck/spec/logCheckConfig.spec.d.ts +1 -0
  18. package/build/commands/logcheck/spec/logCheckConfig.spec.js +323 -0
  19. package/build/commands/logcheck/spec/logcheckCommand.spec.d.ts +1 -0
  20. package/build/commands/logcheck/spec/logcheckCommand.spec.js +360 -0
  21. package/build/commands/logcheck/spec/processLogCheck.spec.d.ts +1 -0
  22. package/build/commands/logcheck/spec/processLogCheck.spec.js +354 -0
  23. package/build/commands/logcheck/spec/renderLogCheck.spec.d.ts +1 -0
  24. package/build/commands/logcheck/spec/renderLogCheck.spec.js +426 -0
  25. package/build/commands/logcheck/types.d.ts +98 -0
  26. package/build/commands/logcheck/types.js +14 -0
  27. package/build/types/command/commandAliases.js +1 -0
  28. package/build/types/command/commandFactory.js +2 -0
  29. package/build/types/command/returnCode.d.ts +2 -1
  30. package/build/types/command/returnCode.js +4 -0
  31. package/package.json +3 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm package][npm-image]][npm-url]
4
4
  [![Github Workflow][githubworkflow-image]][githubworkflow-url]
5
- [![npm](https://img.shields.io/npm/dt/@sasjs/cli)]()
5
+ [![npm](https://img.shields.io/npm/dt/@sasjs/cli)](<>)
6
6
  ![GitHub top language](https://img.shields.io/github/languages/top/sasjs/cli)
7
7
  [![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/sasjs/cli)](https://github.com/sasjs/cli/issues?q=is%3Aissue+is%3Aclosed)
8
8
  [![GitHub issues](https://img.shields.io/github/issues-raw/sasjs/cli)](https://github.com/sasjs/cli/issues)
@@ -23,6 +23,7 @@
23
23
  - Optionally synchronise a local folder onto the server filesystem using `sasjs fs`
24
24
  - Trigger Viya Jobs & Flows (`sasjs job execute` / `sasjs flow execute`)
25
25
  - Enforce code quality rules with `sasjs lint`
26
+ - Check what SAS actually printed, and fail a pipeline on it, with `sasjs logcheck`
26
27
  - Generate HTML5 documentation (requires doxygen) with `sasjs doc`
27
28
  - Create and execute SAS tests (`sasjs test`)
28
29
 
@@ -326,6 +326,15 @@ function printHelpText() {
326
326
  "Provides the capability to identify, for SAS file, whether there are any ERRORs or WARNINGs present and if so, which line number they are on."
327
327
  ]
328
328
  },
329
+ {
330
+ name: 'logcheck',
331
+ title: 'logcheck [paths..]',
332
+ description: [
333
+ "Checks SAS logs for the messages SAS printed - ERRORs, WARNINGs and the notes that flag a problem - and fails when the failure policy is broken.",
334
+ "[2spaces]With no path, the project's results folder is checked. No target, no login and no project are needed, so a log produced elsewhere checks the same way.",
335
+ "[2spaces]command example: sasjs logcheck --fail-on warning --format json"
336
+ ]
337
+ },
329
338
  {
330
339
  name: 'test',
331
340
  title: 'test',
@@ -361,6 +370,7 @@ function printHelpText() {
361
370
  { name: 'deploy', aliases: ['d'] },
362
371
  { name: 'doc', aliases: ['docs'] },
363
372
  { name: 'help', aliases: ['h'] },
373
+ { name: 'logcheck', aliases: ['lc'] },
364
374
  { name: 'request', aliases: ['rq'] },
365
375
  { name: 'run', aliases: ['r'] },
366
376
  { name: 'test', aliases: ['t'] },
@@ -19,3 +19,4 @@ export { createWebAppServices } from './web/web';
19
19
  export { runTest } from './testing/test';
20
20
  export { processLint } from './lint/processLint';
21
21
  export { initLint } from './lint/initLint';
22
+ export { processLogCheck } from './logcheck/processLogCheck';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initLint = exports.processLint = exports.runTest = exports.createWebAppServices = exports.printVersion = exports.runSasCode = exports.runSasJob = exports.printHelpText = exports.deploy = exports.buildDB = exports.initDocs = exports.generateDot = exports.generateDocs = exports.create = exports.buildMockSite = exports.getBuildVars = exports.build = exports.compileSingleFile = exports.compile = exports.addCredential = exports.addTarget = void 0;
3
+ exports.processLogCheck = exports.initLint = exports.processLint = exports.runTest = exports.createWebAppServices = exports.printVersion = exports.runSasCode = exports.runSasJob = exports.printHelpText = exports.deploy = exports.buildDB = exports.initDocs = exports.generateDot = exports.generateDocs = exports.create = exports.buildMockSite = exports.getBuildVars = exports.build = exports.compileSingleFile = exports.compile = exports.addCredential = exports.addTarget = void 0;
4
4
  var addTarget_1 = require("./add/addTarget");
5
5
  Object.defineProperty(exports, "addTarget", { enumerable: true, get: function () { return addTarget_1.addTarget; } });
6
6
  var addCredential_1 = require("./add/addCredential");
@@ -44,3 +44,5 @@ var processLint_1 = require("./lint/processLint");
44
44
  Object.defineProperty(exports, "processLint", { enumerable: true, get: function () { return processLint_1.processLint; } });
45
45
  var initLint_1 = require("./lint/initLint");
46
46
  Object.defineProperty(exports, "initLint", { enumerable: true, get: function () { return initLint_1.initLint; } });
47
+ var processLogCheck_1 = require("./logcheck/processLogCheck");
48
+ Object.defineProperty(exports, "processLogCheck", { enumerable: true, get: function () { return processLogCheck_1.processLogCheck; } });
@@ -0,0 +1,60 @@
1
+ import { LogIssue } from '@sasjs/utils/logs';
2
+ import { LogCheckConfig, ScannedLog, SkippedFile } from './types';
3
+ /** A log bigger than this is reported and skipped rather than read. */
4
+ export declare const MAX_LOG_BYTES: number;
5
+ export interface CollectedFiles {
6
+ /** Absolute paths of the files that will be read, in report order. */
7
+ files: string[];
8
+ /** Paths the user named that do not exist. */
9
+ missing: string[];
10
+ /**
11
+ * Folders that could not be listed, with the reason.
12
+ *
13
+ * A folder the process cannot read is not an empty folder: the logs inside
14
+ * it were never looked at, so it has to reach the report and it has to fail
15
+ * the gate - a check that goes green on a directory it could not open is
16
+ * worse than no check.
17
+ */
18
+ unreadable: SkippedFile[];
19
+ }
20
+ /**
21
+ * Expands the paths a user named into the files to read.
22
+ *
23
+ * A file is taken as it is; a folder is walked, because the reason a user
24
+ * points at `sasjsresults` is that they do not know which run left which log.
25
+ * Order is sorted so that two runs over the same folder produce the same
26
+ * report, which is what makes a diff of two reports readable.
27
+ */
28
+ export declare const collectLogFiles: (paths: string[]) => Promise<CollectedFiles>;
29
+ /**
30
+ * The path a report shows for a file: relative to the project folder where
31
+ * that is shorter and does not climb out of it, absolute otherwise.
32
+ *
33
+ * Forward slashes throughout, so a report generated on Windows and one
34
+ * generated on Linux describe the same log the same way, and so a path can be
35
+ * dropped into a CI annotation without escaping.
36
+ */
37
+ export declare const toDisplayPath: (filePath: string) => string;
38
+ /**
39
+ * Parses one log and splits its issues into the ones reported and the ones
40
+ * suppressed by message code.
41
+ *
42
+ * A file that is not a SAS log is skipped rather than reported as clean: a
43
+ * Python traceback and a shell build log both begin lines with `ERROR:`, and
44
+ * `isSasLog` is what tells them from the real thing.
45
+ */
46
+ export declare const scanLogFile: (filePath: string, config: LogCheckConfig) => Promise<ScannedLog>;
47
+ /**
48
+ * Splits parsed issues by the suppressed message codes.
49
+ *
50
+ * Matching is on the code alone, because that is what SAS keeps stable: the
51
+ * text of a note carries values from the data and changes between runs, while
52
+ * `49-169` means the same thing every time.
53
+ */
54
+ export declare const partitionIssues: (issues: LogIssue[], ignoreCodes: string[]) => {
55
+ issues: LogIssue[];
56
+ suppressed: LogIssue[];
57
+ };
58
+ export declare const countIssues: (issues: LogIssue[]) => import("./types").IssueCounts;
59
+ /** A file the check declined to open, as the report shows it. */
60
+ export declare const toSkippedFile: (log: ScannedLog) => SkippedFile;
@@ -0,0 +1,386 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ var __values = (this && this.__values) || function(o) {
50
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
51
+ if (m) return m.call(o);
52
+ if (o && typeof o.length === "number") return {
53
+ next: function () {
54
+ if (o && i >= o.length) o = void 0;
55
+ return { value: o && o[i++], done: !o };
56
+ }
57
+ };
58
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
59
+ };
60
+ var __read = (this && this.__read) || function (o, n) {
61
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
62
+ if (!m) return o;
63
+ var i = m.call(o), r, ar = [], e;
64
+ try {
65
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
66
+ }
67
+ catch (error) { e = { error: error }; }
68
+ finally {
69
+ try {
70
+ if (r && !r.done && (m = i["return"])) m.call(i);
71
+ }
72
+ finally { if (e) throw e.error; }
73
+ }
74
+ return ar;
75
+ };
76
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
77
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
78
+ if (ar || !(i in from)) {
79
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
80
+ ar[i] = from[i];
81
+ }
82
+ }
83
+ return to.concat(ar || Array.prototype.slice.call(from));
84
+ };
85
+ var __importDefault = (this && this.__importDefault) || function (mod) {
86
+ return (mod && mod.__esModule) ? mod : { "default": mod };
87
+ };
88
+ Object.defineProperty(exports, "__esModule", { value: true });
89
+ exports.toSkippedFile = exports.countIssues = exports.partitionIssues = exports.scanLogFile = exports.toDisplayPath = exports.collectLogFiles = exports.MAX_LOG_BYTES = void 0;
90
+ var path_1 = __importDefault(require("path"));
91
+ var promises_1 = require("fs/promises");
92
+ var logs_1 = require("@sasjs/utils/logs");
93
+ var logCheckConfig_1 = require("./logCheckConfig");
94
+ var types_1 = require("./types");
95
+ /** A log bigger than this is reported and skipped rather than read. */
96
+ exports.MAX_LOG_BYTES = 25 * 1024 * 1024;
97
+ /** Folders never walked when a directory is given. */
98
+ var IGNORED_FOLDERS = ['node_modules', '.git'];
99
+ /**
100
+ * Expands the paths a user named into the files to read.
101
+ *
102
+ * A file is taken as it is; a folder is walked, because the reason a user
103
+ * points at `sasjsresults` is that they do not know which run left which log.
104
+ * Order is sorted so that two runs over the same folder produce the same
105
+ * report, which is what makes a diff of two reports readable.
106
+ */
107
+ var collectLogFiles = function (paths) { return __awaiter(void 0, void 0, void 0, function () {
108
+ var files, missing, unreadable, paths_1, paths_1_1, candidate, resolved, info, _a, _b, found, e_1_1, e_2_1;
109
+ var e_2, _c, e_1, _d;
110
+ return __generator(this, function (_e) {
111
+ switch (_e.label) {
112
+ case 0:
113
+ files = new Set();
114
+ missing = [];
115
+ unreadable = [];
116
+ _e.label = 1;
117
+ case 1:
118
+ _e.trys.push([1, 15, 16, 17]);
119
+ paths_1 = __values(paths), paths_1_1 = paths_1.next();
120
+ _e.label = 2;
121
+ case 2:
122
+ if (!!paths_1_1.done) return [3 /*break*/, 14];
123
+ candidate = paths_1_1.value;
124
+ resolved = path_1.default.resolve(candidate);
125
+ return [4 /*yield*/, (0, promises_1.stat)(resolved).catch(function () { return undefined; })];
126
+ case 3:
127
+ info = _e.sent();
128
+ if (!info) {
129
+ missing.push(candidate);
130
+ return [3 /*break*/, 13];
131
+ }
132
+ if (!info.isDirectory()) return [3 /*break*/, 12];
133
+ _e.label = 4;
134
+ case 4:
135
+ _e.trys.push([4, 9, 10, 11]);
136
+ e_1 = void 0;
137
+ return [4 /*yield*/, walk(resolved, unreadable)];
138
+ case 5:
139
+ _a = (__values.apply(void 0, [_e.sent()])), _b = _a.next();
140
+ _e.label = 6;
141
+ case 6:
142
+ if (!!_b.done) return [3 /*break*/, 8];
143
+ found = _b.value;
144
+ files.add(found);
145
+ _e.label = 7;
146
+ case 7:
147
+ _b = _a.next();
148
+ return [3 /*break*/, 6];
149
+ case 8: return [3 /*break*/, 11];
150
+ case 9:
151
+ e_1_1 = _e.sent();
152
+ e_1 = { error: e_1_1 };
153
+ return [3 /*break*/, 11];
154
+ case 10:
155
+ try {
156
+ if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
157
+ }
158
+ finally { if (e_1) throw e_1.error; }
159
+ return [7 /*endfinally*/];
160
+ case 11: return [3 /*break*/, 13];
161
+ case 12:
162
+ files.add(resolved);
163
+ _e.label = 13;
164
+ case 13:
165
+ paths_1_1 = paths_1.next();
166
+ return [3 /*break*/, 2];
167
+ case 14: return [3 /*break*/, 17];
168
+ case 15:
169
+ e_2_1 = _e.sent();
170
+ e_2 = { error: e_2_1 };
171
+ return [3 /*break*/, 17];
172
+ case 16:
173
+ try {
174
+ if (paths_1_1 && !paths_1_1.done && (_c = paths_1.return)) _c.call(paths_1);
175
+ }
176
+ finally { if (e_2) throw e_2.error; }
177
+ return [7 /*endfinally*/];
178
+ case 17: return [2 /*return*/, {
179
+ files: Array.from(files).sort(function (a, b) { return a.localeCompare(b); }),
180
+ missing: missing,
181
+ unreadable: unreadable
182
+ }];
183
+ }
184
+ });
185
+ }); };
186
+ exports.collectLogFiles = collectLogFiles;
187
+ /**
188
+ * Recursively lists the files under a folder, recording any folder it cannot
189
+ * read rather than treating it as empty.
190
+ *
191
+ * Symlinks are not followed: a link back up the tree would make the walk
192
+ * endless, and a log reached through a link is not something to report on
193
+ * twice.
194
+ */
195
+ var walk = function (folder_1) {
196
+ var args_1 = [];
197
+ for (var _i = 1; _i < arguments.length; _i++) {
198
+ args_1[_i - 1] = arguments[_i];
199
+ }
200
+ return __awaiter(void 0, __spreadArray([folder_1], __read(args_1), false), void 0, function (folder, unreadable) {
201
+ var entries, found, entries_1, entries_1_1, entry, fullPath, _a, _b, _c, _d, e_3_1;
202
+ var e_3, _e;
203
+ if (unreadable === void 0) { unreadable = []; }
204
+ return __generator(this, function (_f) {
205
+ switch (_f.label) {
206
+ case 0: return [4 /*yield*/, (0, promises_1.readdir)(folder, { withFileTypes: true }).catch(function (error) {
207
+ unreadable.push({
208
+ displayPath: (0, exports.toDisplayPath)(folder),
209
+ reason: (error === null || error === void 0 ? void 0 : error.code)
210
+ ? "could not be listed (".concat(error.code, ")")
211
+ : 'could not be listed'
212
+ });
213
+ return [];
214
+ })];
215
+ case 1:
216
+ entries = _f.sent();
217
+ found = [];
218
+ _f.label = 2;
219
+ case 2:
220
+ _f.trys.push([2, 8, 9, 10]);
221
+ entries_1 = __values(entries), entries_1_1 = entries_1.next();
222
+ _f.label = 3;
223
+ case 3:
224
+ if (!!entries_1_1.done) return [3 /*break*/, 7];
225
+ entry = entries_1_1.value;
226
+ if (entry.isSymbolicLink())
227
+ return [3 /*break*/, 6];
228
+ fullPath = path_1.default.join(folder, entry.name);
229
+ if (!entry.isDirectory()) return [3 /*break*/, 5];
230
+ if (IGNORED_FOLDERS.includes(entry.name))
231
+ return [3 /*break*/, 6];
232
+ _b = (_a = found.push).apply;
233
+ _c = [found];
234
+ _d = [[]];
235
+ return [4 /*yield*/, walk(fullPath, unreadable)];
236
+ case 4:
237
+ _b.apply(_a, _c.concat([__spreadArray.apply(void 0, _d.concat([__read.apply(void 0, [(_f.sent())]), false]))]));
238
+ return [3 /*break*/, 6];
239
+ case 5:
240
+ if (entry.isFile())
241
+ found.push(fullPath);
242
+ _f.label = 6;
243
+ case 6:
244
+ entries_1_1 = entries_1.next();
245
+ return [3 /*break*/, 3];
246
+ case 7: return [3 /*break*/, 10];
247
+ case 8:
248
+ e_3_1 = _f.sent();
249
+ e_3 = { error: e_3_1 };
250
+ return [3 /*break*/, 10];
251
+ case 9:
252
+ try {
253
+ if (entries_1_1 && !entries_1_1.done && (_e = entries_1.return)) _e.call(entries_1);
254
+ }
255
+ finally { if (e_3) throw e_3.error; }
256
+ return [7 /*endfinally*/];
257
+ case 10: return [2 /*return*/, found];
258
+ }
259
+ });
260
+ });
261
+ };
262
+ /**
263
+ * The path a report shows for a file: relative to the project folder where
264
+ * that is shorter and does not climb out of it, absolute otherwise.
265
+ *
266
+ * Forward slashes throughout, so a report generated on Windows and one
267
+ * generated on Linux describe the same log the same way, and so a path can be
268
+ * dropped into a CI annotation without escaping.
269
+ */
270
+ var toDisplayPath = function (filePath) {
271
+ var base = process.projectDir || process.cwd();
272
+ var relative = path_1.default.relative(base, filePath);
273
+ return relative && !relative.startsWith('..')
274
+ ? (0, logCheckConfig_1.toPosix)(relative)
275
+ : (0, logCheckConfig_1.toPosix)(filePath);
276
+ };
277
+ exports.toDisplayPath = toDisplayPath;
278
+ /**
279
+ * Parses one log and splits its issues into the ones reported and the ones
280
+ * suppressed by message code.
281
+ *
282
+ * A file that is not a SAS log is skipped rather than reported as clean: a
283
+ * Python traceback and a shell build log both begin lines with `ERROR:`, and
284
+ * `isSasLog` is what tells them from the real thing.
285
+ */
286
+ var scanLogFile = function (filePath, config) { return __awaiter(void 0, void 0, void 0, function () {
287
+ var displayPath, blank, info, text, _a, issues, suppressed;
288
+ return __generator(this, function (_b) {
289
+ switch (_b.label) {
290
+ case 0:
291
+ displayPath = (0, exports.toDisplayPath)(filePath);
292
+ blank = {
293
+ filePath: filePath,
294
+ displayPath: displayPath,
295
+ issues: [],
296
+ suppressed: [],
297
+ counts: (0, types_1.emptyCounts)()
298
+ };
299
+ return [4 /*yield*/, (0, promises_1.stat)(filePath).catch(function () { return undefined; })];
300
+ case 1:
301
+ info = _b.sent();
302
+ if (!info) {
303
+ return [2 /*return*/, __assign(__assign({}, blank), { skippedReason: 'could not be read' })];
304
+ }
305
+ if (!info.isFile()) {
306
+ return [2 /*return*/, __assign(__assign({}, blank), { skippedReason: 'not a file' })];
307
+ }
308
+ if (info.size > exports.MAX_LOG_BYTES) {
309
+ return [2 /*return*/, __assign(__assign({}, blank), { skippedReason: "larger than ".concat(Math.round(exports.MAX_LOG_BYTES / (1024 * 1024)), " MB") })];
310
+ }
311
+ return [4 /*yield*/, (0, promises_1.readFile)(filePath, 'utf8').catch(function () { return undefined; })];
312
+ case 2:
313
+ text = _b.sent();
314
+ if (text === undefined) {
315
+ return [2 /*return*/, __assign(__assign({}, blank), { skippedReason: 'could not be read' })];
316
+ }
317
+ if (!(0, logs_1.isSasLog)(text)) {
318
+ return [2 /*return*/, __assign(__assign({}, blank), { skippedReason: 'not a SAS log' })];
319
+ }
320
+ _a = (0, exports.partitionIssues)((0, logs_1.parseLog)(text), config.ignoreCodes), issues = _a.issues, suppressed = _a.suppressed;
321
+ return [2 /*return*/, __assign(__assign({}, blank), { issues: issues, suppressed: suppressed, counts: (0, exports.countIssues)(issues) })];
322
+ }
323
+ });
324
+ }); };
325
+ exports.scanLogFile = scanLogFile;
326
+ /**
327
+ * Splits parsed issues by the suppressed message codes.
328
+ *
329
+ * Matching is on the code alone, because that is what SAS keeps stable: the
330
+ * text of a note carries values from the data and changes between runs, while
331
+ * `49-169` means the same thing every time.
332
+ */
333
+ var partitionIssues = function (issues, ignoreCodes) {
334
+ var e_4, _a;
335
+ var codes = new Set(ignoreCodes.map(function (code) { return code.trim(); }));
336
+ var kept = [];
337
+ var suppressed = [];
338
+ try {
339
+ for (var issues_1 = __values(issues), issues_1_1 = issues_1.next(); !issues_1_1.done; issues_1_1 = issues_1.next()) {
340
+ var issue = issues_1_1.value;
341
+ if (issue.code && codes.has(issue.code))
342
+ suppressed.push(issue);
343
+ else
344
+ kept.push(issue);
345
+ }
346
+ }
347
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
348
+ finally {
349
+ try {
350
+ if (issues_1_1 && !issues_1_1.done && (_a = issues_1.return)) _a.call(issues_1);
351
+ }
352
+ finally { if (e_4) throw e_4.error; }
353
+ }
354
+ return { issues: kept, suppressed: suppressed };
355
+ };
356
+ exports.partitionIssues = partitionIssues;
357
+ var countIssues = function (issues) {
358
+ var e_5, _a;
359
+ var counts = (0, types_1.emptyCounts)();
360
+ try {
361
+ for (var issues_2 = __values(issues), issues_2_1 = issues_2.next(); !issues_2_1.done; issues_2_1 = issues_2.next()) {
362
+ var issue = issues_2_1.value;
363
+ if (issue.severity === 'error')
364
+ counts.errors++;
365
+ else if (issue.severity === 'warning')
366
+ counts.warnings++;
367
+ else
368
+ counts.notes++;
369
+ }
370
+ }
371
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
372
+ finally {
373
+ try {
374
+ if (issues_2_1 && !issues_2_1.done && (_a = issues_2.return)) _a.call(issues_2);
375
+ }
376
+ finally { if (e_5) throw e_5.error; }
377
+ }
378
+ return counts;
379
+ };
380
+ exports.countIssues = countIssues;
381
+ /** A file the check declined to open, as the report shows it. */
382
+ var toSkippedFile = function (log) { return ({
383
+ displayPath: log.displayPath,
384
+ reason: log.skippedReason || 'skipped'
385
+ }); };
386
+ exports.toSkippedFile = toSkippedFile;
@@ -0,0 +1,72 @@
1
+ import { LogCheckConfig } from './types';
2
+ /** The section of `sasjsconfig.json` this command reads. */
3
+ export declare const CONFIG_KEY = "logCheck";
4
+ /** Folder checked when the command is given no path of its own. */
5
+ export declare const DEFAULT_FOLDER = "sasjsresults";
6
+ /**
7
+ * Builds a config from an untrusted `logCheck` section.
8
+ *
9
+ * Values a user gets wrong are refused rather than ignored: a typo in
10
+ * `failOn` that silently fell back to the default would leave a gate that
11
+ * never fails, which is the one failure mode a quality gate must not have.
12
+ *
13
+ * @param section - the parsed `logCheck` object from `sasjsconfig.json`.
14
+ * @param source - the file it came from, named in the error message.
15
+ */
16
+ export declare const parseLogCheckSection: (section: unknown, source?: string) => LogCheckConfig;
17
+ /**
18
+ * Reads the `logCheck` policy from the project's `sasjsconfig.json`.
19
+ *
20
+ * A missing file, or one without the section, is not an error - the command
21
+ * has to work in a folder that is not a SASjs project at all, which is the
22
+ * whole point of checking a log somebody else produced.
23
+ */
24
+ export declare const readLogCheckConfig: () => Promise<LogCheckConfig>;
25
+ /** Flags a user can set on the command line, all optional. */
26
+ export interface ConfigOverrides {
27
+ failOn?: string;
28
+ maxWarnings?: number;
29
+ ignoreCodes?: string[];
30
+ ignorePaths?: string[];
31
+ }
32
+ /**
33
+ * Layers the command line over the config file. Suppressions are added, never
34
+ * replaced: a flag silences one more code for one run, it does not un-silence
35
+ * the codes a team has already agreed to accept.
36
+ */
37
+ export declare const mergeConfig: (base: LogCheckConfig, overrides?: ConfigOverrides) => LogCheckConfig;
38
+ /**
39
+ * Whether a log path is excluded by one of the `ignorePaths` entries.
40
+ *
41
+ * An entry with no wildcard is matched against whole path segments, not as a
42
+ * bare substring of the text: `test` covers `test/run.log` and
43
+ * `sasjsresults/test/run.log`, but not `latest.log`, and `logs` does not cover
44
+ * `nested-logs/x.log`. A suppression rule that quietly covers files the author
45
+ * did not name is the failure mode this whole config guards against, so an
46
+ * entry has to name a path, not a fragment of one.
47
+ *
48
+ * An entry with a wildcard is a glob over the display path, anchored at both
49
+ * ends, where `**&#47;` crosses folders and `*` does not.
50
+ */
51
+ export declare const matchesPath: (displayPath: string, pattern: string) => boolean;
52
+ /**
53
+ * A non-glob entry matches when it names the path itself, or a run of whole
54
+ * segments inside it.
55
+ *
56
+ * Padding both sides with a separator makes one `includes` do the work: the
57
+ * needle can only be found where it begins at a segment boundary and ends at
58
+ * one, which is exactly the difference between `test` and `latest`.
59
+ */
60
+ export declare const matchesSegments: (subject: string, candidate: string) => boolean;
61
+ export declare const isIgnoredPath: (displayPath: string, patterns: string[]) => boolean;
62
+ /**
63
+ * Compiles a glob into an expression anchored at both ends.
64
+ *
65
+ * `**&#47;` at the start matches zero or more leading folders so that
66
+ * `**&#47;build.log` covers `build.log` in the project folder as well as one
67
+ * several folders down - a pattern that only matched the deep case would make
68
+ * the obvious spelling useless.
69
+ */
70
+ export declare const globToRegExp: (pattern: string) => RegExp;
71
+ /** Path text as it appears in a report: forward slashes, no wrapping quotes. */
72
+ export declare const toPosix: (value: string) => string;