skir 0.0.3 → 0.0.6

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 (60) hide show
  1. package/dist/casing.d.ts.map +1 -1
  2. package/dist/casing.js +4 -1
  3. package/dist/casing.js.map +1 -1
  4. package/dist/casing.test.js +35 -1
  5. package/dist/casing.test.js.map +1 -1
  6. package/dist/compatibility_checker.test.js +2 -2
  7. package/dist/compiler.js +23 -42
  8. package/dist/compiler.js.map +1 -1
  9. package/dist/config.d.ts +2 -2
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +5 -10
  12. package/dist/config.js.map +1 -1
  13. package/dist/config_parser.d.ts +25 -0
  14. package/dist/config_parser.d.ts.map +1 -0
  15. package/dist/config_parser.js +125 -0
  16. package/dist/config_parser.js.map +1 -0
  17. package/dist/config_parser.test.d.ts +2 -0
  18. package/dist/config_parser.test.d.ts.map +1 -0
  19. package/dist/config_parser.test.js +386 -0
  20. package/dist/config_parser.test.js.map +1 -0
  21. package/dist/doc_comment_parser.d.ts +3 -2
  22. package/dist/doc_comment_parser.d.ts.map +1 -1
  23. package/dist/doc_comment_parser.js +67 -52
  24. package/dist/doc_comment_parser.js.map +1 -1
  25. package/dist/doc_comment_parser.test.js +86 -154
  26. package/dist/doc_comment_parser.test.js.map +1 -1
  27. package/dist/error_renderer.d.ts +4 -0
  28. package/dist/error_renderer.d.ts.map +1 -1
  29. package/dist/error_renderer.js +21 -0
  30. package/dist/error_renderer.js.map +1 -1
  31. package/dist/module_set.d.ts.map +1 -1
  32. package/dist/module_set.js +29 -12
  33. package/dist/module_set.js.map +1 -1
  34. package/dist/module_set.test.js +318 -173
  35. package/dist/module_set.test.js.map +1 -1
  36. package/dist/parser.d.ts.map +1 -1
  37. package/dist/parser.js +10 -10
  38. package/dist/parser.js.map +1 -1
  39. package/dist/project_initializer.js +9 -1
  40. package/dist/project_initializer.js.map +1 -1
  41. package/dist/tokenizer.d.ts +7 -1
  42. package/dist/tokenizer.d.ts.map +1 -1
  43. package/dist/tokenizer.js +12 -0
  44. package/dist/tokenizer.js.map +1 -1
  45. package/package.json +10 -5
  46. package/src/casing.ts +6 -1
  47. package/src/compiler.ts +26 -40
  48. package/src/config.ts +10 -15
  49. package/src/config_parser.ts +169 -0
  50. package/src/doc_comment_parser.ts +76 -52
  51. package/src/error_renderer.ts +34 -0
  52. package/src/module_set.ts +31 -15
  53. package/src/parser.ts +16 -10
  54. package/src/project_initializer.ts +9 -1
  55. package/src/tokenizer.ts +20 -2
  56. package/dist/language_server.d.ts +0 -15
  57. package/dist/language_server.d.ts.map +0 -1
  58. package/dist/language_server.js +0 -248
  59. package/dist/language_server.js.map +0 -1
  60. package/src/language_server.ts +0 -301
package/src/compiler.ts CHANGED
@@ -4,15 +4,15 @@ import { glob } from "glob";
4
4
  import * as paths from "path";
5
5
  import type { CodeGenerator } from "skir-internal";
6
6
  import Watcher from "watcher";
7
- import * as yaml from "yaml";
8
- import { fromZodError } from "zod-validation-error";
9
7
  import { parseCommandLine } from "./command_line_parser.js";
10
- import { GeneratorConfig, SkirConfig } from "./config.js";
8
+ import { GeneratorConfig } from "./config.js";
9
+ import { importCodeGenerator, parseSkirConfig } from "./config_parser.js";
11
10
  import {
12
11
  makeGray,
13
12
  makeGreen,
14
13
  makeRed,
15
14
  renderErrors,
15
+ renderSkirConfigErrors,
16
16
  } from "./error_renderer.js";
17
17
  import { formatModule } from "./formatter.js";
18
18
  import { REAL_FILE_SYSTEM } from "./io.js";
@@ -33,32 +33,19 @@ async function makeGeneratorBundle(
33
33
  config: GeneratorConfig,
34
34
  root: string,
35
35
  ): Promise<GeneratorBundle> {
36
- const mod = await import(config.mod);
37
- const generator = mod.GENERATOR;
38
- if (typeof generator !== "object") {
39
- throw new Error(`Cannot import GENERATOR from module ${config.mod}`);
40
- }
41
- // Validate the generator config.
42
- const parsedConfig = generator.configType.safeParse(config.config);
43
- if (!parsedConfig.success) {
44
- const { id } = generator;
45
- console.error(makeRed(`Invalid config for ${id} generator`));
46
- const validationError = fromZodError(parsedConfig.error);
47
- console.error(validationError.toString());
48
- process.exit(1);
49
- }
36
+ const generator = await importCodeGenerator(config.mod);
50
37
  let skiroutDirs: string[];
51
- if (config.skiroutDir === undefined) {
38
+ if (config.outDir === undefined) {
52
39
  skiroutDirs = ["skirout"];
53
- } else if (typeof config.skiroutDir === "string") {
54
- skiroutDirs = [config.skiroutDir];
40
+ } else if (typeof config.outDir === "string") {
41
+ skiroutDirs = [config.outDir];
55
42
  } else {
56
- skiroutDirs = config.skiroutDir;
43
+ skiroutDirs = config.outDir;
57
44
  }
58
45
  skiroutDirs = skiroutDirs.map((d) => paths.join(root, d));
59
46
  return {
60
- generator,
61
- config: parsedConfig.data,
47
+ generator: generator,
48
+ config: config.config,
62
49
  skiroutDirs: skiroutDirs,
63
50
  };
64
51
  }
@@ -362,28 +349,27 @@ async function main(): Promise<void> {
362
349
  }
363
350
  }
364
351
 
365
- // Use an absolute path to make error messages more helpful.
366
- const skirConfigPath = paths.resolve(paths.join(root!, "skir.yml"));
367
- const skirConfigContents = REAL_FILE_SYSTEM.readTextFile(skirConfigPath);
368
- if (skirConfigContents === undefined) {
352
+ let skirConfigPath = paths.join(root!, "skir.yml");
353
+ if (
354
+ !paths.isAbsolute(skirConfigPath) &&
355
+ !/^\.{1,2}[/\\]$/.test(skirConfigPath)
356
+ ) {
357
+ // To make it clear that it's a path, prepend "./"
358
+ skirConfigPath = `.${paths.sep}${skirConfigPath}`;
359
+ }
360
+ const skirConfigCode = REAL_FILE_SYSTEM.readTextFile(skirConfigPath);
361
+ if (skirConfigCode === undefined) {
369
362
  console.error(makeRed(`Cannot find ${skirConfigPath}`));
370
363
  process.exit(1);
371
364
  }
372
365
 
373
- let skirConfig: SkirConfig;
374
- {
375
- // `yaml.parse` fail with a helpful error message, no need to add context.
376
- const parseResult = SkirConfig.safeParse(yaml.parse(skirConfigContents));
377
- if (parseResult.success) {
378
- skirConfig = parseResult.data;
379
- } else {
380
- console.error(makeRed("Invalid skir config"));
381
- console.error(` Path: ${skirConfigPath}`);
382
- const validationError = fromZodError(parseResult.error);
383
- console.error(validationError.toString());
384
- process.exit(1);
385
- }
366
+ const skirConfigResult = await parseSkirConfig(skirConfigCode, "import-mods");
367
+ if (skirConfigResult.errors.length > 0) {
368
+ console.error(makeRed("Invalid skir config"));
369
+ renderSkirConfigErrors(skirConfigResult.errors, { skirConfigPath });
370
+ process.exit(1);
386
371
  }
372
+ const skirConfig = skirConfigResult.skirConfig!;
387
373
 
388
374
  const srcDir = paths.join(root!, skirConfig.srcDir || ".");
389
375
 
package/src/config.ts CHANGED
@@ -1,20 +1,15 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const GeneratorConfig = z
4
- .object({
5
- mod: z.string(),
6
- config: z.any(),
7
- skiroutDir: z
8
- .union([
9
- z
10
- .string()
11
- .regex(/^.*\/skirout$/)
12
- .optional(),
13
- z.array(z.string().regex(/^.*\/skirout$/)),
14
- ])
15
- .optional(),
16
- })
17
- .strict();
3
+ export const GeneratorConfig = z.strictObject({
4
+ mod: z.string(),
5
+ config: z.any(),
6
+ outDir: z
7
+ .union([
8
+ z.string().endsWith("/skirout"),
9
+ z.array(z.string().endsWith("/skirout")),
10
+ ])
11
+ .optional(),
12
+ });
18
13
 
19
14
  export type GeneratorConfig = z.infer<typeof GeneratorConfig>;
20
15
 
@@ -0,0 +1,169 @@
1
+ import * as ccGen from "skir-cc-gen";
2
+ import * as dartGen from "skir-dart-gen";
3
+ import { CodeGenerator } from "skir-internal";
4
+ import * as javaGen from "skir-java-gen";
5
+ import * as kotlinGen from "skir-kotlin-gen";
6
+ import * as pythonGen from "skir-python-gen";
7
+ import * as typescriptGen from "skir-typescript-gen";
8
+ import { LineCounter, parseDocument, Scalar, YAMLMap } from "yaml";
9
+ import { SkirConfig } from "./config.js";
10
+
11
+ export interface SkirConfigResult {
12
+ skirConfig: SkirConfig | undefined;
13
+ errors: readonly SkirConfigError[];
14
+ }
15
+
16
+ export interface SkirConfigError {
17
+ message: string;
18
+ range?: SkirConfigErrorRange;
19
+ }
20
+
21
+ export interface SkirConfigErrorRange {
22
+ start: SkirConfigErrorPos;
23
+ end: SkirConfigErrorPos;
24
+ }
25
+
26
+ export interface SkirConfigErrorPos {
27
+ /** 0-based */
28
+ offset: number;
29
+ /** 1-based */
30
+ lineNumber: number;
31
+ /** 2-based */
32
+ colNumber: number;
33
+ }
34
+
35
+ export async function parseSkirConfig(
36
+ yamlCode: string,
37
+ importMods?: "import-mods",
38
+ ): Promise<SkirConfigResult> {
39
+ const errors: SkirConfigError[] = [];
40
+
41
+ // 1. Parse YAML into a Document object
42
+ const lineCounter = new LineCounter();
43
+ const doc = parseDocument(yamlCode, { lineCounter });
44
+
45
+ const offsetToPos = (offset: number): SkirConfigErrorPos => {
46
+ const pos = lineCounter.linePos(offset);
47
+ return {
48
+ offset: offset,
49
+ lineNumber: pos.line,
50
+ colNumber: pos.col,
51
+ };
52
+ };
53
+ const offsetRangeToRange = (
54
+ start: number,
55
+ end: number,
56
+ ): SkirConfigErrorRange => ({
57
+ start: offsetToPos(start),
58
+ end: offsetToPos(end),
59
+ });
60
+ const pathToRange = (
61
+ path: readonly PropertyKey[],
62
+ ): SkirConfigErrorRange | undefined => {
63
+ const node = doc.getIn(path, true) as Scalar | YAMLMap | undefined;
64
+ if (!node || !node.range) {
65
+ return undefined;
66
+ }
67
+ return offsetRangeToRange(node.range[0], node.range[1]);
68
+ };
69
+
70
+ // Check for YAML parsing errors
71
+ if (doc.errors.length > 0) {
72
+ for (const error of doc.errors) {
73
+ const range = offsetRangeToRange(error.pos[0], error.pos[1]);
74
+ errors.push({
75
+ message: error.message,
76
+ range: range,
77
+ });
78
+ }
79
+ return { skirConfig: undefined, errors: errors };
80
+ }
81
+
82
+ const jsData = doc.toJS();
83
+
84
+ // 2. Validate with Zod schema
85
+ const result = SkirConfig.safeParse(jsData);
86
+
87
+ if (!result.success) {
88
+ for (const issue of result.error.issues) {
89
+ // Map the Zod path to the YAML node
90
+ const range = pathToRange(issue.path);
91
+ errors.push({
92
+ message: issue.message,
93
+ range: range,
94
+ });
95
+ }
96
+ return { skirConfig: undefined, errors: errors };
97
+ }
98
+
99
+ // 3. Validate each generator's config with Zod schema
100
+ for (let i = 0; i < result.data.generators.length; i++) {
101
+ const generatorConfig = result.data.generators[i]!;
102
+ const { mod } = generatorConfig;
103
+ let generator: CodeGenerator<unknown> | undefined;
104
+ if (importMods) {
105
+ try {
106
+ generator = await importCodeGenerator(mod);
107
+ } catch (e) {
108
+ if (e instanceof Error) {
109
+ const range = pathToRange(["generators", i, "mod"]);
110
+ errors.push({
111
+ message: e.message,
112
+ range: range,
113
+ });
114
+ continue;
115
+ } else {
116
+ throw e;
117
+ }
118
+ }
119
+ } else {
120
+ // TODO: rm the casts
121
+ const modToGenerator: Record<string, CodeGenerator<unknown>> = {
122
+ "skir-cc-gen": ccGen.GENERATOR as any as CodeGenerator<unknown>,
123
+ "skir-dart-gen": dartGen.GENERATOR as any as CodeGenerator<unknown>,
124
+ "skir-java-gen": javaGen.GENERATOR as any as CodeGenerator<unknown>,
125
+ "skir-kotlin-gen": kotlinGen.GENERATOR as any as CodeGenerator<unknown>,
126
+ "skir-python-gen": pythonGen.GENERATOR as any as CodeGenerator<unknown>,
127
+ "skir-typescript-gen":
128
+ typescriptGen.GENERATOR as any as CodeGenerator<unknown>,
129
+ };
130
+ generator = modToGenerator[mod];
131
+ }
132
+ if (generator) {
133
+ const parsedGeneratorConfig = generator.configType.safeParse(
134
+ generatorConfig.config,
135
+ );
136
+ if (!parsedGeneratorConfig.success) {
137
+ for (const issue of parsedGeneratorConfig.error.issues) {
138
+ const path: readonly PropertyKey[] = [
139
+ "generators",
140
+ i,
141
+ "config",
142
+ ...issue.path,
143
+ ];
144
+ const range = pathToRange(path);
145
+ errors.push({
146
+ message: issue.message ?? "Error",
147
+ range: range,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (errors.length > 0) {
154
+ return { skirConfig: undefined, errors: errors };
155
+ }
156
+
157
+ return { skirConfig: result.data, errors: [] };
158
+ }
159
+
160
+ export async function importCodeGenerator(
161
+ mod: string,
162
+ ): Promise<CodeGenerator<unknown>> {
163
+ const module = await import(mod);
164
+ const generator = module.GENERATOR;
165
+ if (typeof generator !== "object") {
166
+ throw new Error(`Cannot import GENERATOR from module ${mod}`);
167
+ }
168
+ return generator as CodeGenerator<unknown>;
169
+ }
@@ -3,39 +3,58 @@ import type {
3
3
  Doc,
4
4
  DocPiece,
5
5
  DocReference,
6
+ DocReferenceName,
7
+ MutableDoc,
8
+ MutableDocPiece,
9
+ MutableDocReferenceName,
6
10
  Result,
7
11
  SkirError,
8
12
  Token,
9
13
  } from "skir-internal";
10
14
 
11
- export function parseDocComments(docComments: readonly Token[]): Result<Doc> {
12
- const parser = new DocCommentsParser(docComments);
15
+ export function parseDocComment(docComment: Token): Result<Doc> {
16
+ const parser = new DocCommentParser(docComment);
13
17
  return parser.parse();
14
18
  }
15
19
 
16
- class DocCommentsParser {
20
+ class DocCommentParser {
17
21
  private readonly pieces: DocPiece[] = [];
18
22
  private readonly errors: SkirError[] = [];
19
23
  private currentText = "";
20
- private docCommentIndex = -1;
21
24
  private charIndex = -1;
22
- private contentOffset = -1;
23
-
24
- constructor(private readonly docComments: readonly Token[]) {}
25
+ private readonly content: string;
26
+
27
+ constructor(private readonly docComment: Token) {
28
+ const { text } = docComment;
29
+ if (text.startsWith("/// ")) {
30
+ this.content = text.slice(4);
31
+ } else if (text.startsWith("///")) {
32
+ this.content = text.slice(3);
33
+ } else {
34
+ throw new Error("Expected doc comment to start with ///");
35
+ }
36
+ this.charIndex = 0;
37
+ }
25
38
 
26
39
  parse(): Result<Doc> {
27
- while (this.nextDocComment()) {
28
- this.parseCurrentDocComment();
29
- }
40
+ this.parseDocComment();
30
41
 
31
42
  // Add any remaining text
32
43
  if (this.currentText.length > 0) {
33
44
  this.pieces.push({ kind: "text", text: this.currentText });
34
45
  }
35
46
 
36
- const text = this.docComments
37
- .map((c) => c.text.slice(c.text.startsWith("/// ") ? 4 : 3))
38
- .join("\n");
47
+ const { pieces } = this;
48
+ const text = pieces
49
+ .map((p) => {
50
+ switch (p.kind) {
51
+ case "text":
52
+ return p.text;
53
+ case "reference":
54
+ return p.referenceRange.text;
55
+ }
56
+ })
57
+ .join("");
39
58
 
40
59
  return {
41
60
  result: {
@@ -46,7 +65,7 @@ class DocCommentsParser {
46
65
  };
47
66
  }
48
67
 
49
- private parseCurrentDocComment(): void {
68
+ private parseDocComment(): void {
50
69
  // Matches unescaped [ or ], OR escaped [[ or ]]
51
70
  const specialCharRegex = /\[\[|\]\]|\[|\]/g;
52
71
 
@@ -93,18 +112,15 @@ class DocCommentsParser {
93
112
  this.charIndex++;
94
113
  }
95
114
  }
96
-
97
- // Add newline between comment lines (except after the last line)
98
- if (this.docCommentIndex < this.docComments.length - 1) {
99
- this.currentText += "\n";
100
- }
101
115
  }
102
116
 
103
117
  private parseReference(): DocReference {
104
118
  const { content, docComment } = this;
105
119
 
106
120
  const leftBracketCharIndex = this.charIndex;
107
- const startPosition = docComment.position + leftBracketCharIndex;
121
+ const contentOffset = docComment.text.length - content.length;
122
+ const startPosition =
123
+ docComment.position + contentOffset + leftBracketCharIndex;
108
124
 
109
125
  const rightBracketCharIndex = content.indexOf("]", leftBracketCharIndex);
110
126
 
@@ -139,7 +155,8 @@ class DocCommentsParser {
139
155
  const tokens: Token[] = [];
140
156
  while (this.charIndex < endCharIndex) {
141
157
  const char = content[this.charIndex]!;
142
- const position = docComment.position + this.charIndex;
158
+ const contentOffset = docComment.text.length - content.length;
159
+ const position = docComment.position + contentOffset + this.charIndex;
143
160
 
144
161
  const makeToken = (text: string): Token => ({
145
162
  text: text,
@@ -166,7 +183,9 @@ class DocCommentsParser {
166
183
  this.charIndex++;
167
184
  } else {
168
185
  // Invalid character in reference (including whitespace)
169
- const column = this.docComment.colNumber + this.charIndex;
186
+ const contentOffset = docComment.text.length - content.length;
187
+ const column =
188
+ this.docComment.colNumber + contentOffset + this.charIndex;
170
189
  hasError = true;
171
190
  this.errors.push({
172
191
  token: referenceRange,
@@ -177,11 +196,11 @@ class DocCommentsParser {
177
196
  }
178
197
  }
179
198
 
180
- const nameChain = hasError ? [] : this.parseNameChain(tokens);
199
+ const nameParts = hasError ? [] : this.parseNameParts(tokens);
181
200
 
182
201
  return {
183
202
  kind: "reference",
184
- nameChain: nameChain,
203
+ nameParts: nameParts,
185
204
  absolute: tokens[0]?.text === ".",
186
205
  referee: undefined,
187
206
  docComment: this.docComment,
@@ -189,8 +208,10 @@ class DocCommentsParser {
189
208
  };
190
209
  }
191
210
 
192
- private parseNameChain(tokens: readonly Token[]): Token[] {
193
- const nameChain: Token[] = [];
211
+ private parseNameParts(
212
+ tokens: readonly Token[],
213
+ ): readonly DocReferenceName[] {
214
+ const nameParts: MutableDocReferenceName[] = [];
194
215
  let expect: "identifier" | "identifier or '.'" | "'.' or ']'" =
195
216
  "identifier or '.'";
196
217
  for (const token of tokens) {
@@ -198,7 +219,10 @@ class DocCommentsParser {
198
219
  if (/^[a-zA-Z]/.test(token.text)) {
199
220
  expected = expect === "identifier or '.'" || expect === "identifier";
200
221
  expect = "'.' or ']'";
201
- nameChain.push(token);
222
+ nameParts.push({
223
+ token: token,
224
+ declaration: undefined,
225
+ });
202
226
  } else if (token.text === ".") {
203
227
  expected = expect === "identifier or '.'" || expect === "'.' or ']'";
204
228
  expect = "identifier";
@@ -214,38 +238,38 @@ class DocCommentsParser {
214
238
  return [];
215
239
  }
216
240
  if (token.text === "]") {
217
- return nameChain;
241
+ return nameParts;
218
242
  }
219
243
  }
220
244
  // An error has already been pushed to signify the unterminated reference.
221
245
  return [];
222
246
  }
247
+ }
223
248
 
224
- /// The current doc comment being parsed.
225
- private get docComment(): Token {
226
- return this.docComments[this.docCommentIndex]!;
227
- }
228
-
229
- /// The text of the current doc comment being parsed.
230
- private get content(): string {
231
- return this.docComment.text;
249
+ export function mergeDocs(docs: readonly Doc[]): MutableDoc {
250
+ if (docs.length <= 0) {
251
+ return EMPTY_DOC;
232
252
  }
233
-
234
- private nextDocComment(): boolean {
235
- if (this.docCommentIndex < this.docComments.length - 1) {
236
- this.docCommentIndex++;
237
- const { content } = this;
238
- if (content.startsWith("/// ")) {
239
- this.contentOffset = 4;
240
- } else if (content.startsWith("///")) {
241
- this.contentOffset = 3;
242
- } else {
243
- throw new Error("Expected doc comment to start with ///");
244
- }
245
- this.charIndex = this.contentOffset;
246
- return true;
247
- } else {
248
- return false;
253
+ // Insert '\n' between each doc comment (== line)
254
+ const text = docs.map((d) => d.text).join("\n");
255
+ const pieces: MutableDocPiece[] = [];
256
+ for (let i = 0; i < docs.length; ++i) {
257
+ const doc = docs[i]!;
258
+ if (i !== 0) {
259
+ pieces.push({
260
+ kind: "text",
261
+ text: "\n",
262
+ });
249
263
  }
264
+ doc.pieces.forEach((p) => pieces.push(p));
250
265
  }
266
+ return {
267
+ text: text,
268
+ pieces: pieces,
269
+ };
251
270
  }
271
+
272
+ const EMPTY_DOC: Doc = {
273
+ text: "",
274
+ pieces: [],
275
+ };
@@ -11,6 +11,7 @@ import {
11
11
  Expression,
12
12
  getTokenForBreakingChange,
13
13
  } from "./compatibility_checker.js";
14
+ import { SkirConfigError } from "./config_parser.js";
14
15
  import { ModuleSet } from "./module_set.js";
15
16
 
16
17
  export function renderErrors(errors: readonly SkirError[]): void {
@@ -56,6 +57,39 @@ export function formatError(error: SkirError): string {
56
57
  return result;
57
58
  }
58
59
 
60
+ export function renderSkirConfigErrors(
61
+ errors: readonly SkirConfigError[],
62
+ context: {
63
+ skirConfigPath: string;
64
+ },
65
+ ): void {
66
+ for (const error of errors) {
67
+ console.error();
68
+ console.error(formatSkirConfigError(error, context));
69
+ }
70
+ console.error();
71
+ }
72
+
73
+ function formatSkirConfigError(
74
+ error: SkirConfigError,
75
+ context: {
76
+ skirConfigPath: string;
77
+ },
78
+ ): string {
79
+ const { message, range } = error;
80
+ const { skirConfigPath } = context;
81
+ const location = range
82
+ ? [
83
+ makeCyan(skirConfigPath),
84
+ // Already 1-based
85
+ makeYellow(range.start.lineNumber.toString()),
86
+ // Already 1-based
87
+ makeYellow(range.start.colNumber.toString()),
88
+ ].join(":")
89
+ : makeCyan(skirConfigPath);
90
+ return [location, " - ", makeRed("error"), ": ", message].join("");
91
+ }
92
+
59
93
  export function renderBreakingChanges(
60
94
  breakingChanges: readonly BreakingChange[],
61
95
  moduleSet: BeforeAfter<ModuleSet>,