pbiplint 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/pbiplint.mjs +20 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# pbiplint
|
|
2
2
|
|
|
3
3
|
Best-practice linter for Power BI semantic models. Point it at a `.SemanticModel` folder, a PBIP
|
|
4
|
-
folder, or one `.tmdl` file and get ranked findings with a link to a fix
|
|
5
|
-
Nothing is uploaded: it reads the files you name and writes to your terminal.
|
|
4
|
+
folder, a `definition` folder, or one `.tmdl` file and get ranked findings with a link to a fix
|
|
5
|
+
page for each rule. Nothing is uploaded: it reads the files you name and writes to your terminal.
|
|
6
|
+
Node 20 or later.
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
9
|
npx pbiplint path/to/Model.SemanticModel
|
|
@@ -10,12 +11,15 @@ npx pbiplint --sample # a bundled model wi
|
|
|
10
11
|
npx pbiplint path/to/model --format sarif --output pbiplint.sarif
|
|
11
12
|
npx pbiplint path/to/model --format markdown
|
|
12
13
|
npx pbiplint rules # every rule with status and severity
|
|
14
|
+
npx pbiplint --help # every option, in one screen
|
|
15
|
+
npx pbiplint --version
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
Formats: `text` (default), `json`, `sarif` (for GitHub code scanning and editors), `markdown`.
|
|
16
19
|
|
|
17
|
-
Exit codes: `0` no findings at or above `--fail-on
|
|
18
|
-
|
|
20
|
+
Exit codes: `0` no findings at or above `--fail-on`, `1` findings, `2` usage or input error.
|
|
21
|
+
`--fail-on error` is the default; `--fail-on warning` and `--fail-on info` tighten the gate;
|
|
22
|
+
`--fail-on none` always exits 0.
|
|
19
23
|
|
|
20
24
|
## Configuration
|
|
21
25
|
|
package/dist/pbiplint.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { basename as basename2, dirname as dirname4, relative as relative2, resolve as resolve3 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// ../core/src/version.ts
|
|
8
|
-
var VERSION = "0.1.
|
|
8
|
+
var VERSION = "0.1.2";
|
|
9
9
|
|
|
10
10
|
// ../core/src/engine/config.ts
|
|
11
11
|
var ConfigError = class extends Error {
|
|
@@ -36,6 +36,8 @@ function resolveConfig(raw = {}) {
|
|
|
36
36
|
for (const k of Object.keys(raw))
|
|
37
37
|
if (k !== "rules" && k !== "failOn" && k !== "$schema")
|
|
38
38
|
throw new ConfigError(`pbiplint.config.json: unknown key "${k}"`);
|
|
39
|
+
if (raw.$schema !== void 0 && typeof raw.$schema !== "string")
|
|
40
|
+
throw new ConfigError('pbiplint.config.json: "$schema" must be a string');
|
|
39
41
|
const out = { disabled: /* @__PURE__ */ new Set(), severity: /* @__PURE__ */ new Map(), failOn: 3 };
|
|
40
42
|
if (raw.rules !== void 0) {
|
|
41
43
|
if (!isRecord(raw.rules))
|
|
@@ -2320,35 +2322,31 @@ function parseTmdl(file, text) {
|
|
|
2320
2322
|
const roots = [];
|
|
2321
2323
|
const issues = [];
|
|
2322
2324
|
const stack = [];
|
|
2323
|
-
let pendingDescription =
|
|
2324
|
-
|
|
2325
|
-
let descriptionText = "";
|
|
2326
|
-
const orphanDescription = () => {
|
|
2325
|
+
let pendingDescription = null;
|
|
2326
|
+
const orphanDescription = (pending) => {
|
|
2327
2327
|
issues.push({
|
|
2328
2328
|
file,
|
|
2329
|
-
line:
|
|
2330
|
-
text:
|
|
2329
|
+
line: pending.line,
|
|
2330
|
+
text: pending.text,
|
|
2331
2331
|
reason: "description is not followed by a declaration"
|
|
2332
2332
|
});
|
|
2333
|
-
pendingDescription =
|
|
2333
|
+
pendingDescription = null;
|
|
2334
2334
|
};
|
|
2335
2335
|
let i = 0;
|
|
2336
2336
|
while (i < lines.length) {
|
|
2337
2337
|
const raw = lines[i];
|
|
2338
2338
|
const lineNo = i + 1;
|
|
2339
2339
|
if (raw.trim() === "") {
|
|
2340
|
-
if (pendingDescription
|
|
2340
|
+
if (pendingDescription) orphanDescription(pendingDescription);
|
|
2341
2341
|
i++;
|
|
2342
2342
|
continue;
|
|
2343
2343
|
}
|
|
2344
2344
|
const indent = tabIndent(raw);
|
|
2345
2345
|
const content = raw.slice(indent);
|
|
2346
2346
|
if (content.startsWith("///")) {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
}
|
|
2351
|
-
pendingDescription.push(content.replace(/^\/\/\/ ?/, ""));
|
|
2347
|
+
const line = content.replace(/^\/\/\/ ?/, "");
|
|
2348
|
+
if (pendingDescription) pendingDescription.lines.push(line);
|
|
2349
|
+
else pendingDescription = { line: lineNo, text: raw, lines: [line] };
|
|
2352
2350
|
i++;
|
|
2353
2351
|
continue;
|
|
2354
2352
|
}
|
|
@@ -2425,9 +2423,9 @@ function parseTmdl(file, text) {
|
|
|
2425
2423
|
node = { ...base, kind: "flag", type: h.type.toLowerCase() };
|
|
2426
2424
|
}
|
|
2427
2425
|
}
|
|
2428
|
-
if (pendingDescription
|
|
2429
|
-
node.description = pendingDescription.join("\n");
|
|
2430
|
-
pendingDescription =
|
|
2426
|
+
if (pendingDescription) {
|
|
2427
|
+
node.description = pendingDescription.lines.join("\n");
|
|
2428
|
+
pendingDescription = null;
|
|
2431
2429
|
}
|
|
2432
2430
|
stack.length = indent;
|
|
2433
2431
|
const parent = indent > 0 ? stack[indent - 1] : void 0;
|
|
@@ -2446,7 +2444,7 @@ function parseTmdl(file, text) {
|
|
|
2446
2444
|
stack[indent] = node;
|
|
2447
2445
|
i++;
|
|
2448
2446
|
}
|
|
2449
|
-
if (pendingDescription
|
|
2447
|
+
if (pendingDescription) orphanDescription(pendingDescription);
|
|
2450
2448
|
return { file, roots, issues, lineCount: lines.length };
|
|
2451
2449
|
}
|
|
2452
2450
|
|
|
@@ -2492,7 +2490,7 @@ function rank(findings, rules, config) {
|
|
|
2492
2490
|
g.findings.push(f);
|
|
2493
2491
|
}
|
|
2494
2492
|
return [...groups.values()].sort(
|
|
2495
|
-
(a, b) => b.rule.severity - a.rule.severity || CATEGORY_ORDER.indexOf(a.rule.category) - CATEGORY_ORDER.indexOf(b.rule.category) || b.findings.length - a.findings.length || a.rule.id.localeCompare(b.rule.id)
|
|
2493
|
+
(a, b) => b.rule.severity - a.rule.severity || CATEGORY_ORDER.indexOf(a.rule.category) - CATEGORY_ORDER.indexOf(b.rule.category) || b.findings.length - a.findings.length || a.rule.id.localeCompare(b.rule.id, "en")
|
|
2496
2494
|
);
|
|
2497
2495
|
}
|
|
2498
2496
|
|
|
@@ -3142,7 +3140,7 @@ Read more: https://pbiplint.com/rules/large-tables-should-be-partitioned`,
|
|
|
3142
3140
|
markdown: "### Why it matters\n\nThe description is the tooltip a report author sees when hovering a field in the field list, and it is the only place in the model to say what a measure counts, which currency a column is in, or which of two similar fields to use. Without it, every author works that out from the name, and gets it wrong at about the same rate. Descriptions also feed documentation tools, so the same sentence pays off twice.\n\n### How to fix it\n\nIn Power BI Desktop, open Model view, select the object, and type the Description in the Properties pane. In the TMDL file a description is one or more `///` lines directly above the object. With hundreds of objects, Tabular Editor can paste descriptions into many objects at once; the TMDL comment lines need no other tool.\n\n### Quirks\n\n- Visibility is the object's own isHidden flag: a visible column inside a hidden table is still reported.\n- A calculation group table is reported once, as a calculation group.\n\nRead more: https://pbiplint.com/rules/objects-with-no-description"
|
|
3143
3141
|
},
|
|
3144
3142
|
PARSE_ISSUE: {
|
|
3145
|
-
text: "Why it matters\n\nThe parser skipped the line, so whatever it declared, a column, a property, a measure, is missing from the model the rules see. Findings on that object and on anything that references it may be missing or wrong, and a result that looks clean may not be. The orphaned description is the mild case: no declaration is lost, only the description, which stops at the blank line instead of reaching the object below it, so that object is read as having none. Tabular Editor's TMDL reader is stricter and refuses to open a file that puts a blank line after a /// line at all.\n\nHow to fix it\n\nOpen the file at the reported line. TMDL is indented with tabs, and expression blocks open and close with
|
|
3143
|
+
text: "Why it matters\n\nThe parser skipped the line, so whatever it declared, a column, a property, a measure, is missing from the model the rules see. Findings on that object and on anything that references it may be missing or wrong, and a result that looks clean may not be. The orphaned description is the mild case: no declaration is lost, only the description, which stops at the blank line instead of reaching the object below it, so that object is read as having none. Tabular Editor's TMDL reader is stricter and refuses to open a file that puts a blank line after a /// line at all.\n\nHow to fix it\n\nOpen the file at the reported line. TMDL is indented with tabs, and expression blocks open and close with ``` on their own lines. A /// description must sit directly above its declaration, with no blank line between them. Power BI Desktop writes valid TMDL, so a parse issue usually means a hand edit or a merge conflict marker.\n\nRead more: https://pbiplint.com/rules/parse-issue",
|
|
3146
3144
|
markdown: "### Why it matters\n\nThe parser skipped the line, so whatever it declared, a column, a property, a measure, is missing from the model the rules see. Findings on that object and on anything that references it may be missing or wrong, and a result that looks clean may not be. The orphaned description is the mild case: no declaration is lost, only the description, which stops at the blank line instead of reaching the object below it, so that object is read as having none. Tabular Editor's TMDL reader is stricter and refuses to open a file that puts a blank line after a `///` line at all.\n\n### How to fix it\n\nOpen the file at the reported line. TMDL is indented with tabs, and expression blocks open and close with ``` on their own lines. A `///` description must sit directly above its declaration, with no blank line between them. Power BI Desktop writes valid TMDL, so a parse issue usually means a hand edit or a merge conflict marker.\n\nRead more: https://pbiplint.com/rules/parse-issue"
|
|
3147
3145
|
},
|
|
3148
3146
|
PARTITION_NAME_SHOULD_MATCH_TABLE_NAME_FOR_SINGLE_PARTITION_TABLES: {
|
|
@@ -3249,7 +3247,7 @@ import { basename, dirname as dirname3, join as join3, relative, resolve as reso
|
|
|
3249
3247
|
var toPosix = (p) => p.split("\\").join("/");
|
|
3250
3248
|
function readTmdlFiles(root, dir, out) {
|
|
3251
3249
|
for (const entry of readdirSync(dir, { withFileTypes: true }).sort(
|
|
3252
|
-
(a, b) => a.name.localeCompare(b.name)
|
|
3250
|
+
(a, b) => a.name.localeCompare(b.name, "en")
|
|
3253
3251
|
)) {
|
|
3254
3252
|
const p = join3(dir, entry.name);
|
|
3255
3253
|
if (entry.isDirectory()) readTmdlFiles(root, p, out);
|
|
@@ -3290,7 +3288,7 @@ function resolveModel(input) {
|
|
|
3290
3288
|
}
|
|
3291
3289
|
|
|
3292
3290
|
// src/main.ts
|
|
3293
|
-
var VERSION2 = true ? "0.1.
|
|
3291
|
+
var VERSION2 = true ? "0.1.2" : "0.0.0-dev";
|
|
3294
3292
|
function listRules() {
|
|
3295
3293
|
const width = Math.max(...defaultRules.map((r) => r.id.length));
|
|
3296
3294
|
return defaultRules.map(
|
package/package.json
CHANGED