@pbiplint/core 0.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.
Files changed (189) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +29 -0
  3. package/README.md +34 -0
  4. package/dist/engine/config.d.ts +33 -0
  5. package/dist/engine/config.d.ts.map +1 -0
  6. package/dist/engine/config.js +61 -0
  7. package/dist/engine/config.js.map +1 -0
  8. package/dist/engine/ignore.d.ts +8 -0
  9. package/dist/engine/ignore.d.ts.map +1 -0
  10. package/dist/engine/ignore.js +17 -0
  11. package/dist/engine/ignore.js.map +1 -0
  12. package/dist/engine/lint.d.ts +38 -0
  13. package/dist/engine/lint.d.ts.map +1 -0
  14. package/dist/engine/lint.js +33 -0
  15. package/dist/engine/lint.js.map +1 -0
  16. package/dist/engine/rank.d.ts +24 -0
  17. package/dist/engine/rank.d.ts.map +1 -0
  18. package/dist/engine/rank.js +38 -0
  19. package/dist/engine/rank.js.map +1 -0
  20. package/dist/engine/run.d.ts +21 -0
  21. package/dist/engine/run.d.ts.map +1 -0
  22. package/dist/engine/run.js +43 -0
  23. package/dist/engine/run.js.map +1 -0
  24. package/dist/format/index.d.ts +11 -0
  25. package/dist/format/index.d.ts.map +1 -0
  26. package/dist/format/index.js +21 -0
  27. package/dist/format/index.js.map +1 -0
  28. package/dist/format/json.d.ts +4 -0
  29. package/dist/format/json.d.ts.map +1 -0
  30. package/dist/format/json.js +20 -0
  31. package/dist/format/json.js.map +1 -0
  32. package/dist/format/markdown.d.ts +4 -0
  33. package/dist/format/markdown.d.ts.map +1 -0
  34. package/dist/format/markdown.js +28 -0
  35. package/dist/format/markdown.js.map +1 -0
  36. package/dist/format/sarif.d.ts +4 -0
  37. package/dist/format/sarif.d.ts.map +1 -0
  38. package/dist/format/sarif.js +77 -0
  39. package/dist/format/sarif.js.map +1 -0
  40. package/dist/format/text.d.ts +31 -0
  41. package/dist/format/text.d.ts.map +1 -0
  42. package/dist/format/text.js +57 -0
  43. package/dist/format/text.js.map +1 -0
  44. package/dist/index/build.d.ts +11 -0
  45. package/dist/index/build.d.ts.map +1 -0
  46. package/dist/index/build.js +11 -0
  47. package/dist/index/build.js.map +1 -0
  48. package/dist/index/references.d.ts +37 -0
  49. package/dist/index/references.d.ts.map +1 -0
  50. package/dist/index/references.js +126 -0
  51. package/dist/index/references.js.map +1 -0
  52. package/dist/index/relationships.d.ts +10 -0
  53. package/dist/index/relationships.d.ts.map +1 -0
  54. package/dist/index/relationships.js +24 -0
  55. package/dist/index/relationships.js.map +1 -0
  56. package/dist/index/usage.d.ts +11 -0
  57. package/dist/index/usage.d.ts.map +1 -0
  58. package/dist/index/usage.js +25 -0
  59. package/dist/index/usage.js.map +1 -0
  60. package/dist/index.d.ts +25 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +21 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/model/build.d.ts +9 -0
  65. package/dist/model/build.d.ts.map +1 -0
  66. package/dist/model/build.js +282 -0
  67. package/dist/model/build.js.map +1 -0
  68. package/dist/model/names.d.ts +12 -0
  69. package/dist/model/names.d.ts.map +1 -0
  70. package/dist/model/names.js +21 -0
  71. package/dist/model/names.js.map +1 -0
  72. package/dist/model/types.d.ts +150 -0
  73. package/dist/model/types.d.ts.map +1 -0
  74. package/dist/model/types.js +2 -0
  75. package/dist/model/types.js.map +1 -0
  76. package/dist/rules/helpers.d.ts +54 -0
  77. package/dist/rules/helpers.d.ts.map +1 -0
  78. package/dist/rules/helpers.js +189 -0
  79. package/dist/rules/helpers.js.map +1 -0
  80. package/dist/rules/index.d.ts +3 -0
  81. package/dist/rules/index.d.ts.map +1 -0
  82. package/dist/rules/index.js +4 -0
  83. package/dist/rules/index.js.map +1 -0
  84. package/dist/rules/microsoft-bpa/bpa-rules.data.d.ts +12 -0
  85. package/dist/rules/microsoft-bpa/bpa-rules.data.d.ts.map +1 -0
  86. package/dist/rules/microsoft-bpa/bpa-rules.data.js +664 -0
  87. package/dist/rules/microsoft-bpa/bpa-rules.data.js.map +1 -0
  88. package/dist/rules/microsoft-bpa/columns.d.ts +14 -0
  89. package/dist/rules/microsoft-bpa/columns.d.ts.map +1 -0
  90. package/dist/rules/microsoft-bpa/columns.js +117 -0
  91. package/dist/rules/microsoft-bpa/columns.js.map +1 -0
  92. package/dist/rules/microsoft-bpa/define.d.ts +14 -0
  93. package/dist/rules/microsoft-bpa/define.d.ts.map +1 -0
  94. package/dist/rules/microsoft-bpa/define.js +75 -0
  95. package/dist/rules/microsoft-bpa/define.js.map +1 -0
  96. package/dist/rules/microsoft-bpa/dependencies.d.ts +11 -0
  97. package/dist/rules/microsoft-bpa/dependencies.d.ts.map +1 -0
  98. package/dist/rules/microsoft-bpa/dependencies.js +56 -0
  99. package/dist/rules/microsoft-bpa/dependencies.js.map +1 -0
  100. package/dist/rules/microsoft-bpa/index.d.ts +4 -0
  101. package/dist/rules/microsoft-bpa/index.d.ts.map +1 -0
  102. package/dist/rules/microsoft-bpa/index.js +20 -0
  103. package/dist/rules/microsoft-bpa/index.js.map +1 -0
  104. package/dist/rules/microsoft-bpa/live-model.d.ts +3 -0
  105. package/dist/rules/microsoft-bpa/live-model.d.ts.map +1 -0
  106. package/dist/rules/microsoft-bpa/live-model.js +10 -0
  107. package/dist/rules/microsoft-bpa/live-model.js.map +1 -0
  108. package/dist/rules/microsoft-bpa/measures.d.ts +15 -0
  109. package/dist/rules/microsoft-bpa/measures.d.ts.map +1 -0
  110. package/dist/rules/microsoft-bpa/measures.js +69 -0
  111. package/dist/rules/microsoft-bpa/measures.js.map +1 -0
  112. package/dist/rules/microsoft-bpa/naming.d.ts +14 -0
  113. package/dist/rules/microsoft-bpa/naming.d.ts.map +1 -0
  114. package/dist/rules/microsoft-bpa/naming.js +47 -0
  115. package/dist/rules/microsoft-bpa/naming.js.map +1 -0
  116. package/dist/rules/microsoft-bpa/relationships.d.ts +14 -0
  117. package/dist/rules/microsoft-bpa/relationships.d.ts.map +1 -0
  118. package/dist/rules/microsoft-bpa/relationships.js +97 -0
  119. package/dist/rules/microsoft-bpa/relationships.js.map +1 -0
  120. package/dist/rules/microsoft-bpa/tables.d.ts +16 -0
  121. package/dist/rules/microsoft-bpa/tables.d.ts.map +1 -0
  122. package/dist/rules/microsoft-bpa/tables.js +147 -0
  123. package/dist/rules/microsoft-bpa/tables.js.map +1 -0
  124. package/dist/rules/parse-issue.d.ts +4 -0
  125. package/dist/rules/parse-issue.d.ts.map +1 -0
  126. package/dist/rules/parse-issue.js +19 -0
  127. package/dist/rules/parse-issue.js.map +1 -0
  128. package/dist/rules/rule-summaries.data.d.ts +3 -0
  129. package/dist/rules/rule-summaries.data.d.ts.map +1 -0
  130. package/dist/rules/rule-summaries.data.js +78 -0
  131. package/dist/rules/rule-summaries.data.js.map +1 -0
  132. package/dist/rules/types.d.ts +42 -0
  133. package/dist/rules/types.d.ts.map +1 -0
  134. package/dist/rules/types.js +15 -0
  135. package/dist/rules/types.js.map +1 -0
  136. package/dist/tmdl/parse.d.ts +7 -0
  137. package/dist/tmdl/parse.d.ts.map +1 -0
  138. package/dist/tmdl/parse.js +196 -0
  139. package/dist/tmdl/parse.js.map +1 -0
  140. package/dist/tmdl/quote.d.ts +5 -0
  141. package/dist/tmdl/quote.d.ts.map +1 -0
  142. package/dist/tmdl/quote.js +15 -0
  143. package/dist/tmdl/quote.js.map +1 -0
  144. package/dist/tmdl/types.d.ts +32 -0
  145. package/dist/tmdl/types.d.ts.map +1 -0
  146. package/dist/tmdl/types.js +2 -0
  147. package/dist/tmdl/types.js.map +1 -0
  148. package/dist/version.d.ts +2 -0
  149. package/dist/version.d.ts.map +1 -0
  150. package/dist/version.js +3 -0
  151. package/dist/version.js.map +1 -0
  152. package/package.json +51 -0
  153. package/src/engine/config.ts +91 -0
  154. package/src/engine/ignore.ts +18 -0
  155. package/src/engine/lint.ts +79 -0
  156. package/src/engine/rank.ts +67 -0
  157. package/src/engine/run.ts +67 -0
  158. package/src/format/index.ts +37 -0
  159. package/src/format/json.ts +22 -0
  160. package/src/format/markdown.ts +37 -0
  161. package/src/format/sarif.ts +85 -0
  162. package/src/format/text.ts +90 -0
  163. package/src/index/build.ts +18 -0
  164. package/src/index/references.ts +170 -0
  165. package/src/index/relationships.ts +33 -0
  166. package/src/index/usage.ts +33 -0
  167. package/src/index.ts +80 -0
  168. package/src/model/build.ts +306 -0
  169. package/src/model/names.ts +31 -0
  170. package/src/model/types.ts +165 -0
  171. package/src/rules/helpers.ts +223 -0
  172. package/src/rules/index.ts +5 -0
  173. package/src/rules/microsoft-bpa/bpa-rules.data.ts +782 -0
  174. package/src/rules/microsoft-bpa/columns.ts +192 -0
  175. package/src/rules/microsoft-bpa/define.ts +84 -0
  176. package/src/rules/microsoft-bpa/dependencies.ts +95 -0
  177. package/src/rules/microsoft-bpa/index.ts +22 -0
  178. package/src/rules/microsoft-bpa/live-model.ts +10 -0
  179. package/src/rules/microsoft-bpa/measures.ts +143 -0
  180. package/src/rules/microsoft-bpa/naming.ts +104 -0
  181. package/src/rules/microsoft-bpa/relationships.ts +189 -0
  182. package/src/rules/microsoft-bpa/tables.ts +249 -0
  183. package/src/rules/parse-issue.ts +23 -0
  184. package/src/rules/rule-summaries.data.ts +144 -0
  185. package/src/rules/types.ts +87 -0
  186. package/src/tmdl/parse.ts +194 -0
  187. package/src/tmdl/quote.ts +15 -0
  188. package/src/tmdl/types.ts +34 -0
  189. package/src/version.ts +2 -0
@@ -0,0 +1,196 @@
1
+ import { unquoteName, unquoteValue } from "./quote.js";
2
+ const HEADER = /^([A-Za-z_]\w*)(?:\s+(.+))?$/;
3
+ const PROP = /^([A-Za-z_]\w*):(?:\s(.*))?$/;
4
+ const REF = /^ref\s+([A-Za-z_]\w*)\s+(.+)$/;
5
+ const tabIndent = (line) => {
6
+ let n = 0;
7
+ while (line[n] === "\t")
8
+ n++;
9
+ return n;
10
+ };
11
+ const leadingWs = (line) => line.length - line.trimStart().length;
12
+ /** Split `<type> <name> [= expr]` on the first `=` outside single quotes. */
13
+ function splitHeader(content) {
14
+ let inQuote = false;
15
+ let eqAt = -1;
16
+ for (let i = 0; i < content.length; i++) {
17
+ const ch = content[i];
18
+ if (ch === "'")
19
+ inQuote = !inQuote;
20
+ else if (ch === "=" && !inQuote) {
21
+ eqAt = i;
22
+ break;
23
+ }
24
+ }
25
+ const left = (eqAt >= 0 ? content.slice(0, eqAt) : content).trim();
26
+ const inline = eqAt >= 0 ? content.slice(eqAt + 1).trim() : "";
27
+ const m = HEADER.exec(left);
28
+ if (!m)
29
+ return null;
30
+ return {
31
+ type: m[1],
32
+ name: m[2] === undefined ? undefined : unquoteName(m[2]),
33
+ hasEq: eqAt >= 0,
34
+ inline,
35
+ };
36
+ }
37
+ /**
38
+ * Generic TMDL tree parser. Unknown object types and properties parse as generic nodes,
39
+ * so a construct this code has never seen never aborts a run.
40
+ */
41
+ export function parseTmdl(file, text) {
42
+ // Power BI Desktop writes TMDL as UTF-8 with a BOM; it is not part of the first line.
43
+ const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
44
+ const lines = body.replace(/\r\n?/g, "\n").split("\n");
45
+ const roots = [];
46
+ const issues = [];
47
+ const stack = [];
48
+ let pendingDescription = [];
49
+ // The first `///` line of the pending run, for the issue reported when it leads nowhere.
50
+ let descriptionLine = 0;
51
+ let descriptionText = "";
52
+ let i = 0;
53
+ while (i < lines.length) {
54
+ const raw = lines[i];
55
+ const lineNo = i + 1;
56
+ if (raw.trim() === "") {
57
+ // Tabular Editor's TMDL reader rejects a blank line after a `///` description, so a
58
+ // description separated from its declaration never reaches it. Report and drop it.
59
+ if (pendingDescription.length) {
60
+ issues.push({
61
+ file,
62
+ line: descriptionLine,
63
+ text: descriptionText,
64
+ reason: "description is not followed by a declaration",
65
+ });
66
+ pendingDescription = [];
67
+ }
68
+ i++;
69
+ continue;
70
+ }
71
+ const indent = tabIndent(raw);
72
+ const content = raw.slice(indent);
73
+ if (content.startsWith("///")) {
74
+ if (!pendingDescription.length) {
75
+ descriptionLine = lineNo;
76
+ descriptionText = raw;
77
+ }
78
+ pendingDescription.push(content.replace(/^\/\/\/ ?/, ""));
79
+ i++;
80
+ continue;
81
+ }
82
+ if (/^\s/.test(content)) {
83
+ issues.push({
84
+ file,
85
+ line: lineNo,
86
+ text: raw,
87
+ reason: "space indentation (TMDL requires tabs)",
88
+ });
89
+ i++;
90
+ continue;
91
+ }
92
+ // Indented multi-line expression. The first non-blank line after the header sets the block
93
+ // indentation; the block continues while lines are blank or indented at least that much.
94
+ const collectBlock = () => {
95
+ let j = i + 1;
96
+ while (j < lines.length && lines[j].trim() === "")
97
+ j++;
98
+ if (j >= lines.length)
99
+ return "";
100
+ const blockIndent = leadingWs(lines[j]);
101
+ if (blockIndent <= indent)
102
+ return "";
103
+ const out = [];
104
+ let lastNonBlank = -1;
105
+ for (; j < lines.length; j++) {
106
+ const l = lines[j];
107
+ if (l.trim() === "") {
108
+ out.push("");
109
+ continue;
110
+ }
111
+ if (leadingWs(l) < blockIndent)
112
+ break;
113
+ out.push(l.slice(blockIndent));
114
+ lastNonBlank = out.length - 1;
115
+ }
116
+ i = j - 1;
117
+ return out.slice(0, lastNonBlank + 1).join("\n");
118
+ };
119
+ // Fenced expression: header ends with ```; closed by a line that is only ```; that closing
120
+ // line's leading whitespace is the left boundary stripped from every line.
121
+ const collectFenced = () => {
122
+ const out = [];
123
+ let j = i + 1;
124
+ while (j < lines.length && lines[j].trim() !== "```") {
125
+ out.push(lines[j]);
126
+ j++;
127
+ }
128
+ if (j >= lines.length)
129
+ issues.push({ file, line: lineNo, text: raw, reason: "unterminated ``` fence" });
130
+ const boundary = j < lines.length ? leadingWs(lines[j]) : 0;
131
+ i = j;
132
+ return out.map((l) => l.slice(Math.min(boundary, leadingWs(l)))).join("\n");
133
+ };
134
+ const base = {
135
+ props: {},
136
+ children: [],
137
+ file,
138
+ line: lineNo,
139
+ indent,
140
+ };
141
+ let node;
142
+ let m;
143
+ if ((m = REF.exec(content))) {
144
+ node = { ...base, kind: "ref", type: m[1].toLowerCase(), name: unquoteName(m[2]) };
145
+ }
146
+ else if ((m = PROP.exec(content))) {
147
+ node = { ...base, kind: "prop", type: m[1].toLowerCase(), value: unquoteValue(m[2] ?? "") };
148
+ }
149
+ else {
150
+ const h = splitHeader(content);
151
+ if (!h) {
152
+ issues.push({ file, line: lineNo, text: raw, reason: "unrecognized line" });
153
+ i++;
154
+ continue;
155
+ }
156
+ if (h.hasEq) {
157
+ const value = h.inline === "```" ? collectFenced() : h.inline === "" ? collectBlock() : h.inline;
158
+ node =
159
+ h.name === undefined
160
+ ? { ...base, kind: "expr", type: h.type.toLowerCase(), value }
161
+ : { ...base, kind: "object", type: h.type.toLowerCase(), name: h.name, value };
162
+ }
163
+ else if (h.name !== undefined) {
164
+ node = { ...base, kind: "object", type: h.type.toLowerCase(), name: h.name };
165
+ }
166
+ else {
167
+ node = { ...base, kind: "flag", type: h.type.toLowerCase() };
168
+ }
169
+ }
170
+ if (pendingDescription.length) {
171
+ node.description = pendingDescription.join("\n");
172
+ pendingDescription = [];
173
+ }
174
+ stack.length = indent;
175
+ const parent = indent > 0 ? stack[indent - 1] : undefined;
176
+ if (indent > 0 && !parent) {
177
+ issues.push({ file, line: lineNo, text: raw, reason: "orphan indentation" });
178
+ i++;
179
+ continue;
180
+ }
181
+ if (parent) {
182
+ if (node.kind === "prop" || node.kind === "expr")
183
+ parent.props[node.type] = node.value ?? "";
184
+ else if (node.kind === "flag")
185
+ parent.props[node.type] = true;
186
+ parent.children.push(node);
187
+ }
188
+ else {
189
+ roots.push(node);
190
+ }
191
+ stack[indent] = node;
192
+ i++;
193
+ }
194
+ return { file, roots, issues, lineCount: lines.length };
195
+ }
196
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/tmdl/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Strip single quotes from a TMDL object name; `''` inside is one quote. */
2
+ export declare function unquoteName(s: string): string;
3
+ /** Strip double quotes from a TMDL property value; `""` inside is one quote. */
4
+ export declare function unquoteValue(s: string): string;
5
+ //# sourceMappingURL=quote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.d.ts","sourceRoot":"","sources":["../../src/tmdl/quote.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK9C"}
@@ -0,0 +1,15 @@
1
+ /** Strip single quotes from a TMDL object name; `''` inside is one quote. */
2
+ export function unquoteName(s) {
3
+ const t = s.trim();
4
+ if (t.length >= 2 && t.startsWith("'") && t.endsWith("'"))
5
+ return t.slice(1, -1).replace(/''/g, "'");
6
+ return t;
7
+ }
8
+ /** Strip double quotes from a TMDL property value; `""` inside is one quote. */
9
+ export function unquoteValue(s) {
10
+ const t = s.trim();
11
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
12
+ return t.slice(1, -1).replace(/""/g, '"');
13
+ return t;
14
+ }
15
+ //# sourceMappingURL=quote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.js","sourceRoot":"","sources":["../../src/tmdl/quote.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnB,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QACvD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC;AACX,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnB,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QACvD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,32 @@
1
+ export type TmdlNodeKind = "object" | "prop" | "flag" | "ref" | "expr";
2
+ /** One line of TMDL and everything indented beneath it. `type` and `props` keys are lowercased. */
3
+ export interface TmdlNode {
4
+ kind: TmdlNodeKind;
5
+ /** Object type (`table`, `column`), property key (`datatype`), flag (`ishidden`), or ref target type. */
6
+ type: string;
7
+ /** Unquoted object name, for `object` and `ref` nodes. */
8
+ name?: string;
9
+ /** Property value (unquoted) for `prop`; expression text for `expr` and for `object` nodes declared with `=`. */
10
+ value?: string;
11
+ /** Child properties, flags, and expressions by lowercased key. Flags are `true`. */
12
+ props: Record<string, string | true>;
13
+ children: TmdlNode[];
14
+ /** Joined `///` lines that preceded the declaration. */
15
+ description?: string;
16
+ file: string;
17
+ line: number;
18
+ indent: number;
19
+ }
20
+ export interface ParseIssue {
21
+ file: string;
22
+ line: number;
23
+ text: string;
24
+ reason: string;
25
+ }
26
+ export interface ParsedFile {
27
+ file: string;
28
+ roots: TmdlNode[];
29
+ issues: ParseIssue[];
30
+ lineCount: number;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tmdl/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvE,mGAAmG;AACnG,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,yGAAyG;IACzG,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tmdl/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.1.0";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,OAAO,UAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/sync-version.mjs from packages/core/package.json. Do not edit by hand.
2
+ export const VERSION = "0.1.0";
3
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@pbiplint/core",
3
+ "version": "0.1.0",
4
+ "description": "Best-practice linter core for Power BI semantic models (TMDL). Runs in the browser and in Node, makes no network calls.",
5
+ "keywords": [
6
+ "power-bi",
7
+ "tmdl",
8
+ "semantic-model",
9
+ "pbip",
10
+ "linter",
11
+ "best-practices",
12
+ "tabular"
13
+ ],
14
+ "license": "AGPL-3.0-or-later",
15
+ "author": "McKinley Consulting",
16
+ "homepage": "https://pbiplint.com",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/pbiplint/pbiplint.git",
20
+ "directory": "packages/core"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/pbiplint/pbiplint/issues"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "README.md",
38
+ "NOTICE",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json && node scripts/copy-notices.mjs",
46
+ "prepack": "npm run build"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ }
51
+ }
@@ -0,0 +1,91 @@
1
+ import type { Rule, Severity } from "../rules/types.js";
2
+
3
+ export type SeverityName = "info" | "warning" | "error";
4
+
5
+ /** Shape of pbiplint.config.json. */
6
+ export interface PbiplintConfig {
7
+ /** Optional JSON Schema URL, so an editor can validate and complete the file. Ignored here. */
8
+ $schema?: string;
9
+ /** Per rule: "off" disables it; a severity name overrides its severity. */
10
+ rules?: Record<string, "off" | SeverityName>;
11
+ /** Lowest severity that makes the CLI exit nonzero. Default "error". */
12
+ failOn?: SeverityName | "none";
13
+ }
14
+
15
+ export interface ResolvedConfig {
16
+ disabled: Set<string>;
17
+ severity: Map<string, Severity>;
18
+ failOn: Severity | null;
19
+ }
20
+
21
+ export class ConfigError extends Error {}
22
+
23
+ export interface BoundConfig {
24
+ /** The same settings keyed by the rules' own ids. */
25
+ config: ResolvedConfig;
26
+ /** Ids from the config that match no rule, as written. */
27
+ unknownRules: string[];
28
+ }
29
+
30
+ /**
31
+ * Map the config's rule ids onto `rules` without regard to case, so `hide_foreign_keys` reaches
32
+ * HIDE_FOREIGN_KEYS, and list the ids that reach nothing so a typo never disables a rule silently.
33
+ */
34
+ export function bindConfig(config: ResolvedConfig, rules: Rule[]): BoundConfig {
35
+ const idByUpper = new Map(rules.map((r) => [r.id.toUpperCase(), r.id]));
36
+ const bound: ResolvedConfig = { disabled: new Set(), severity: new Map(), failOn: config.failOn };
37
+ const unknownRules: string[] = [];
38
+ for (const id of config.disabled) {
39
+ const real = idByUpper.get(id.toUpperCase());
40
+ if (real === undefined) unknownRules.push(id);
41
+ else bound.disabled.add(real);
42
+ }
43
+ for (const [id, severity] of config.severity) {
44
+ const real = idByUpper.get(id.toUpperCase());
45
+ if (real === undefined) unknownRules.push(id);
46
+ else bound.severity.set(real, severity);
47
+ }
48
+ return { config: bound, unknownRules };
49
+ }
50
+
51
+ export const SEVERITY_BY_NAME: Record<SeverityName, Severity> = { info: 1, warning: 2, error: 3 };
52
+
53
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
54
+ typeof v === "object" && v !== null && !Array.isArray(v);
55
+
56
+ export function isResolvedConfig(v: unknown): v is ResolvedConfig {
57
+ return isRecord(v) && v.disabled instanceof Set && v.severity instanceof Map;
58
+ }
59
+
60
+ export function resolveConfig(raw: unknown = {}): ResolvedConfig {
61
+ if (!isRecord(raw)) throw new ConfigError("pbiplint.config.json must be a JSON object");
62
+ for (const k of Object.keys(raw))
63
+ if (k !== "rules" && k !== "failOn" && k !== "$schema")
64
+ throw new ConfigError(`pbiplint.config.json: unknown key "${k}"`);
65
+ const out: ResolvedConfig = { disabled: new Set(), severity: new Map(), failOn: 3 };
66
+ if (raw.rules !== undefined) {
67
+ if (!isRecord(raw.rules))
68
+ throw new ConfigError(
69
+ 'pbiplint.config.json: "rules" must be an object of rule id to "off" | "info" | "warning" | "error"',
70
+ );
71
+ for (const [id, v] of Object.entries(raw.rules)) {
72
+ if (v === "off") out.disabled.add(id);
73
+ else if (v === "info" || v === "warning" || v === "error")
74
+ out.severity.set(id, SEVERITY_BY_NAME[v]);
75
+ else
76
+ throw new ConfigError(
77
+ `pbiplint.config.json: rules["${id}"] must be "off", "info", "warning", or "error"`,
78
+ );
79
+ }
80
+ }
81
+ if (raw.failOn !== undefined) {
82
+ if (raw.failOn === "none") out.failOn = null;
83
+ else if (raw.failOn === "info" || raw.failOn === "warning" || raw.failOn === "error")
84
+ out.failOn = SEVERITY_BY_NAME[raw.failOn];
85
+ else
86
+ throw new ConfigError(
87
+ 'pbiplint.config.json: "failOn" must be "info", "warning", "error", or "none"',
88
+ );
89
+ }
90
+ return out;
91
+ }
@@ -0,0 +1,18 @@
1
+ import type { Named } from "../model/types.js";
2
+ import { unquoteValue } from "../tmdl/quote.js";
3
+
4
+ export const IGNORE_ANNOTATION = "pbiplint.ignore";
5
+
6
+ /**
7
+ * True when the object carries `annotation pbiplint.ignore = RULE_A, RULE_B` naming this rule, or
8
+ * `= *`. Ids are matched without regard to case, so a hand-typed lowercase id still works.
9
+ */
10
+ export function isIgnored(object: Named | undefined, ruleId: string): boolean {
11
+ const raw = object?.annotations[IGNORE_ANNOTATION];
12
+ if (raw === undefined) return false;
13
+ const wanted = ruleId.toUpperCase();
14
+ return unquoteValue(raw)
15
+ .split(",")
16
+ .map((s) => s.trim())
17
+ .some((s) => s === "*" || s.toUpperCase() === wanted);
18
+ }
@@ -0,0 +1,79 @@
1
+ import { buildIndexes } from "../index/build.js";
2
+ import { buildModel } from "../model/build.js";
3
+ import type { Model } from "../model/types.js";
4
+ import { defaultRules } from "../rules/index.js";
5
+ import type { Finding, Rule } from "../rules/types.js";
6
+ import { parseTmdl } from "../tmdl/parse.js";
7
+ import {
8
+ bindConfig,
9
+ isResolvedConfig,
10
+ resolveConfig,
11
+ type PbiplintConfig,
12
+ type ResolvedConfig,
13
+ } from "./config.js";
14
+ import { rank, type RankedGroup } from "./rank.js";
15
+ import { runRules, type RuleError, type SkippedRule } from "./run.js";
16
+
17
+ export interface LintFile {
18
+ /** Path relative to the model root, forward slashes, e.g. `definition/tables/Sales.tmdl`. */
19
+ path: string;
20
+ text: string;
21
+ }
22
+
23
+ export interface LintOptions {
24
+ config?: PbiplintConfig | ResolvedConfig;
25
+ rules?: Rule[];
26
+ }
27
+
28
+ export interface LintSummary {
29
+ files: number;
30
+ findings: number;
31
+ errors: number;
32
+ warnings: number;
33
+ infos: number;
34
+ rulesRun: number;
35
+ rulesSkipped: SkippedRule[];
36
+ ruleErrors: RuleError[];
37
+ ignored: number;
38
+ /** Rule ids named in the config that match no rule, as written there. */
39
+ unknownRules: string[];
40
+ }
41
+
42
+ export interface LintResult {
43
+ model: Model;
44
+ findings: Finding[];
45
+ groups: RankedGroup[];
46
+ summary: LintSummary;
47
+ /** True when any finding's effective severity is at or above the configured failOn. */
48
+ failed: boolean;
49
+ }
50
+
51
+ /** The one call the web app and the CLI both make. Pure: no I/O, no network. */
52
+ export function lint(files: LintFile[], options: LintOptions = {}): LintResult {
53
+ const rules = options.rules ?? defaultRules;
54
+ const { config, unknownRules } = bindConfig(
55
+ isResolvedConfig(options.config) ? options.config : resolveConfig(options.config),
56
+ rules,
57
+ );
58
+ const parsed = files.map((f) => parseTmdl(f.path, f.text));
59
+ const model = buildModel(parsed);
60
+ const indexes = buildIndexes(model);
61
+ const run = runRules(model, indexes, rules, config);
62
+ const groups = rank(run.findings, rules, config);
63
+ const count = (severity: number) =>
64
+ groups.filter((g) => g.rule.severity === severity).reduce((n, g) => n + g.findings.length, 0);
65
+ const summary: LintSummary = {
66
+ files: files.length,
67
+ findings: run.findings.length,
68
+ errors: count(3),
69
+ warnings: count(2),
70
+ infos: count(1),
71
+ rulesRun: run.rulesRun.length,
72
+ rulesSkipped: run.rulesSkipped,
73
+ ruleErrors: run.ruleErrors,
74
+ ignored: run.ignored,
75
+ unknownRules,
76
+ };
77
+ const failed = config.failOn !== null && groups.some((g) => g.rule.severity >= config.failOn!);
78
+ return { model, findings: run.findings, groups, summary, failed };
79
+ }
@@ -0,0 +1,67 @@
1
+ import { ruleUrl, slug } from "../model/names.js";
2
+ import {
3
+ CATEGORY_ORDER,
4
+ type Category,
5
+ type Finding,
6
+ type Rule,
7
+ type RuleStatus,
8
+ type Severity,
9
+ } from "../rules/types.js";
10
+ import type { ResolvedConfig } from "./config.js";
11
+
12
+ export interface RuleSummary {
13
+ id: string;
14
+ name: string;
15
+ category: Category;
16
+ /** Effective severity after config overrides. */
17
+ severity: Severity;
18
+ slug: string;
19
+ url: string;
20
+ status: RuleStatus;
21
+ }
22
+
23
+ export interface RankedGroup {
24
+ rule: RuleSummary;
25
+ findings: Finding[];
26
+ }
27
+
28
+ export const effectiveSeverity = (rule: Rule, config: ResolvedConfig): Severity =>
29
+ config.severity.get(rule.id) ?? rule.severity;
30
+
31
+ export function summarizeRule(rule: Rule, config: ResolvedConfig): RuleSummary {
32
+ return {
33
+ id: rule.id,
34
+ name: rule.name,
35
+ category: rule.category,
36
+ severity: effectiveSeverity(rule, config),
37
+ slug: slug(rule.id),
38
+ url: ruleUrl(rule.id),
39
+ status: rule.status,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Group findings by rule and order the groups by severity (error first), category priority,
45
+ * finding count (more first), then rule id. Findings inside a group keep model order.
46
+ */
47
+ export function rank(findings: Finding[], rules: Rule[], config: ResolvedConfig): RankedGroup[] {
48
+ const byId = new Map(rules.map((r) => [r.id, r]));
49
+ const groups = new Map<string, RankedGroup>();
50
+ for (const f of findings) {
51
+ let g = groups.get(f.ruleId);
52
+ if (!g) {
53
+ const rule = byId.get(f.ruleId);
54
+ if (!rule) throw new Error(`Finding for unknown rule ${f.ruleId}`);
55
+ g = { rule: summarizeRule(rule, config), findings: [] };
56
+ groups.set(f.ruleId, g);
57
+ }
58
+ g.findings.push(f);
59
+ }
60
+ return [...groups.values()].sort(
61
+ (a, b) =>
62
+ b.rule.severity - a.rule.severity ||
63
+ CATEGORY_ORDER.indexOf(a.rule.category) - CATEGORY_ORDER.indexOf(b.rule.category) ||
64
+ b.findings.length - a.findings.length ||
65
+ a.rule.id.localeCompare(b.rule.id),
66
+ );
67
+ }