@docker-doctor/cli 0.4.3 → 0.4.4

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.
@@ -33,7 +33,7 @@ node_path = __toESM(node_path, 1);
33
33
  let yaml = require("yaml");
34
34
 
35
35
  //#region package.json
36
- var version = "0.4.3";
36
+ var version = "0.4.4";
37
37
 
38
38
  //#endregion
39
39
  //#region ../core/src/project-info/discover.ts
@@ -136,7 +136,6 @@ const processHeredocLine = (state, trimmed) => {
136
136
  };
137
137
  const processInstructionLine = (state, trimmed, lineNum) => {
138
138
  let lineContent = trimmed;
139
- if (lineContent.startsWith("#")) return;
140
139
  const hasContinuation = lineContent.endsWith("\\");
141
140
  if (hasContinuation) lineContent = lineContent.slice(0, -1).trim();
142
141
  if (state.currentInstruction) state.currentArgs += (state.currentArgs ? " " : "") + lineContent;
@@ -159,7 +158,8 @@ const parseDockerfile = (content) => {
159
158
  const trimmed = rawLine.trim();
160
159
  const lineNum = i + 1;
161
160
  const insideHeredoc = state.heredocQueue.length > 0;
162
- if (!state.currentInstruction && !insideHeredoc && (trimmed === "" || trimmed.startsWith("#"))) continue;
161
+ if (!insideHeredoc && trimmed.startsWith("#")) continue;
162
+ if (!state.currentInstruction && !insideHeredoc && trimmed === "") continue;
163
163
  state.rawAccumulator.push(rawLine);
164
164
  if (insideHeredoc) processHeredocLine(state, trimmed);
165
165
  else processInstructionLine(state, trimmed, lineNum);
@@ -202,6 +202,40 @@ const parseCompose = (content, filepath) => {
202
202
  });
203
203
  }
204
204
  };
205
+ /**
206
+ * Builds a {@link ComposeLocator} over the same source text a compose object
207
+ * was parsed from, so rules can attach line numbers to their diagnostics.
208
+ *
209
+ * Keys pulled in via YAML merge keys (`<<: *anchor`) have no concrete node
210
+ * at the merge site, so paths through them resolve to `undefined` — callers
211
+ * fall back to an unnumbered diagnostic, which matches the old behavior.
212
+ */
213
+ const createComposeLocator = (content) => {
214
+ const lineCounter = new yaml.LineCounter();
215
+ const doc = (0, yaml.parseDocument)(content, {
216
+ lineCounter,
217
+ merge: true
218
+ });
219
+ return (path) => {
220
+ let node = doc.contents;
221
+ let offset;
222
+ for (const segment of path) {
223
+ if ((0, yaml.isAlias)(node)) node = node.resolve(doc);
224
+ if ((0, yaml.isMap)(node)) {
225
+ const pair = node.items.find((item) => (0, yaml.isScalar)(item.key) && String(item.key.value) === String(segment));
226
+ if (!pair || !(0, yaml.isScalar)(pair.key)) return;
227
+ offset = pair.key.range?.[0];
228
+ node = pair.value;
229
+ } else if ((0, yaml.isSeq)(node) && typeof segment === "number") {
230
+ const item = node.items[segment];
231
+ if (item === void 0 || item === null) return;
232
+ offset = item.range?.[0];
233
+ node = item;
234
+ } else return;
235
+ }
236
+ return offset === void 0 ? void 0 : lineCounter.linePos(offset).line;
237
+ };
238
+ };
205
239
 
206
240
  //#endregion
207
241
  //#region ../core/src/parsers/exec-form.ts
@@ -277,8 +311,8 @@ const collectStageAliases = (instructions) => {
277
311
  };
278
312
 
279
313
  //#endregion
280
- //#region ../core/src/rules/best-practices.ts
281
- const createDiagnostic$4 = (file, ruleKey, severity, message, help, line) => ({
314
+ //#region ../core/src/rules/create-diagnostic.ts
315
+ const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
282
316
  file,
283
317
  help,
284
318
  line,
@@ -286,12 +320,15 @@ const createDiagnostic$4 = (file, ruleKey, severity, message, help, line) => ({
286
320
  rule: ruleKey,
287
321
  severity
288
322
  });
323
+
324
+ //#endregion
325
+ //#region ../core/src/rules/best-practices.ts
289
326
  const requireHealthcheck = {
290
327
  category: "Best Practices",
291
328
  check(instructions, file) {
292
329
  const hasHealthcheck = instructions.some((inst) => inst.instruction === "HEALTHCHECK");
293
330
  const hasExposedPortsOrEntry = instructions.some((inst) => inst.instruction === "EXPOSE" || inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT");
294
- if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic$4(file, this.key, this.defaultSeverity, "No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.", this.help, 1)];
331
+ if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic(file, this.key, this.defaultSeverity, "No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.", this.help, 1)];
295
332
  return [];
296
333
  },
297
334
  defaultSeverity: "info",
@@ -308,7 +345,7 @@ const preferCopyOverAdd = {
308
345
  if (!src) continue;
309
346
  const isRemote = src.startsWith("http://") || src.startsWith("https://");
310
347
  const isArchive = src.endsWith(".tar") || src.endsWith(".tar.gz") || src.endsWith(".tgz") || src.endsWith(".zip");
311
- if (!isRemote && !isArchive) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`, this.help, inst.line));
348
+ if (!isRemote && !isArchive) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`, this.help, inst.line));
312
349
  }
313
350
  return diagnostics;
314
351
  },
@@ -321,7 +358,7 @@ const useExecForm = {
321
358
  category: "Best Practices",
322
359
  check(instructions, file) {
323
360
  const diagnostics = [];
324
- for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`, this.help, inst.line));
361
+ for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`, this.help, inst.line));
325
362
  return diagnostics;
326
363
  },
327
364
  defaultSeverity: "warning",
@@ -332,7 +369,7 @@ const useExecForm = {
332
369
  const requireLabels = {
333
370
  category: "Best Practices",
334
371
  check(instructions, file) {
335
- if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic$4(file, this.key, this.defaultSeverity, "No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.", this.help, 1)];
372
+ if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic(file, this.key, this.defaultSeverity, "No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.", this.help, 1)];
336
373
  return [];
337
374
  },
338
375
  defaultSeverity: "info",
@@ -347,8 +384,8 @@ const combineAptUpdateInstall = {
347
384
  for (const inst of instructions) if (inst.instruction === "RUN") {
348
385
  const hasUpdate = inst.args.includes("apt-get update");
349
386
  const hasInstall = inst.args.includes("apt-get install");
350
- if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.", this.help, inst.line));
351
- else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.", this.help, inst.line));
387
+ if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.", this.help, inst.line));
388
+ else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.", this.help, inst.line));
352
389
  }
353
390
  return diagnostics;
354
391
  },
@@ -388,7 +425,7 @@ const usePipefail = {
388
425
  const { args } = inst;
389
426
  if (!HAS_PIPE_RE.test(args)) continue;
390
427
  const execArgv = parseExecForm(args);
391
- if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.", this.help, inst.line));
428
+ if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.", this.help, inst.line));
392
429
  }
393
430
  return diagnostics;
394
431
  },
@@ -403,7 +440,7 @@ const absoluteWorkdir = {
403
440
  const diagnostics = [];
404
441
  for (const inst of instructions) if (inst.instruction === "WORKDIR") {
405
442
  const path = inst.args.trim();
406
- if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`, this.help, inst.line));
443
+ if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`, this.help, inst.line));
407
444
  }
408
445
  return diagnostics;
409
446
  },
@@ -416,7 +453,7 @@ const avoidRunCd = {
416
453
  category: "Best Practices",
417
454
  check(instructions, file) {
418
455
  const diagnostics = [];
419
- for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
456
+ for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
420
457
  return diagnostics;
421
458
  },
422
459
  defaultSeverity: "info",
@@ -436,7 +473,7 @@ const sortMultilineArgs = {
436
473
  const packages = raw.split(/\r?\n/u).slice(1).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("&&") && !line.startsWith("-") && !line.includes("rm -rf")).map((line) => line.endsWith("\\") ? line.slice(0, -1).trim() : line).filter(Boolean);
437
474
  if (packages.length > 1) {
438
475
  const sorted = packages.toSorted((a, b) => a.localeCompare(b));
439
- if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.", this.help, inst.line));
476
+ if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.", this.help, inst.line));
440
477
  }
441
478
  }
442
479
  }
@@ -451,7 +488,7 @@ const useraddNoLogInit = {
451
488
  category: "Best Practices",
452
489
  check(instructions, file) {
453
490
  const diagnostics = [];
454
- for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic$4(file, this.key, this.defaultSeverity, "RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.", this.help, inst.line));
491
+ for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.", this.help, inst.line));
455
492
  return diagnostics;
456
493
  },
457
494
  defaultSeverity: "warning",
@@ -474,17 +511,10 @@ const bestPracticesRules = [
474
511
 
475
512
  //#endregion
476
513
  //#region ../core/src/rules/compose.ts
477
- const createDiagnostic$3 = (file, ruleKey, severity, message, help) => ({
478
- file,
479
- help,
480
- message,
481
- rule: ruleKey,
482
- severity
483
- });
484
514
  const noVersionKey = {
485
515
  category: "Compose",
486
- check(composeContent, file) {
487
- if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic$3(file, this.key, this.defaultSeverity, "The 'version' property is deprecated. Remove it to use standard Compose spec behavior.", this.help)];
516
+ check(composeContent, file, context) {
517
+ if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic(file, this.key, this.defaultSeverity, "The 'version' property is deprecated. Remove it to use standard Compose spec behavior.", this.help, context?.locate?.(["version"]))];
488
518
  return [];
489
519
  },
490
520
  defaultSeverity: "warning",
@@ -494,14 +524,14 @@ const noVersionKey = {
494
524
  };
495
525
  const requireResourceLimits = {
496
526
  category: "Compose",
497
- check(composeContent, file) {
527
+ check(composeContent, file, context) {
498
528
  const diagnostics = [];
499
529
  if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
500
530
  const { services } = composeContent;
501
531
  if (services && typeof services === "object") {
502
532
  for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
503
533
  const limits = (config.deploy?.resources)?.limits;
504
- if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic$3(file, this.key, this.defaultSeverity, `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`, this.help));
534
+ if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`, this.help, context?.locate?.(["services", name])));
505
535
  }
506
536
  }
507
537
  }
@@ -514,7 +544,7 @@ const requireResourceLimits = {
514
544
  };
515
545
  const requireRestartPolicy = {
516
546
  category: "Compose",
517
- check(composeContent, file) {
547
+ check(composeContent, file, context) {
518
548
  const diagnostics = [];
519
549
  if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
520
550
  const { services } = composeContent;
@@ -522,7 +552,7 @@ const requireRestartPolicy = {
522
552
  for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
523
553
  const hasRestart = "restart" in config;
524
554
  const hasDeployRestart = config.deploy?.restart_policy !== void 0;
525
- if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic$3(file, this.key, this.defaultSeverity, `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`, this.help));
555
+ if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`, this.help, context?.locate?.(["services", name])));
526
556
  }
527
557
  }
528
558
  }
@@ -535,14 +565,18 @@ const requireRestartPolicy = {
535
565
  };
536
566
  const useDependsOnCondition = {
537
567
  category: "Compose",
538
- check(composeContent, file) {
568
+ check(composeContent, file, context) {
539
569
  const diagnostics = [];
540
570
  if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
541
571
  const { services } = composeContent;
542
572
  if (services && typeof services === "object") {
543
573
  for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
544
574
  const dependsOn = config.depends_on;
545
- if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic$3(file, this.key, this.defaultSeverity, `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`, this.help));
575
+ if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`, this.help, context?.locate?.([
576
+ "services",
577
+ name,
578
+ "depends_on"
579
+ ]) ?? context?.locate?.(["services", name])));
546
580
  }
547
581
  }
548
582
  }
@@ -562,14 +596,6 @@ const composeRules = [
562
596
 
563
597
  //#endregion
564
598
  //#region ../core/src/rules/image-size.ts
565
- const createDiagnostic$2 = (file, ruleKey, severity, message, help, line) => ({
566
- file,
567
- help,
568
- line,
569
- message,
570
- rule: ruleKey,
571
- severity
572
- });
573
599
  const preferSlimBase = {
574
600
  category: "Image Size",
575
601
  check(instructions, file) {
@@ -583,7 +609,7 @@ const preferSlimBase = {
583
609
  if (ref.digest) continue;
584
610
  if (!ref.tag) continue;
585
611
  const haystack = `${ref.name} ${ref.tag}`.toLowerCase();
586
- if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic$2(file, this.key, this.defaultSeverity, `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`, this.help, inst.line));
612
+ if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`, this.help, inst.line));
587
613
  }
588
614
  return diagnostics;
589
615
  },
@@ -592,14 +618,20 @@ const preferSlimBase = {
592
618
  key: "docker-doctor/prefer-slim-base",
593
619
  message: "Prefer slim, alpine, or distroless base images"
594
620
  };
621
+ const BUILDKIT_MOUNT_FLAG_RE = /--mount=(?<spec>\S+)/gu;
622
+ const CACHE_TARGET_KEY_RE = /^(?:target|dst|destination)=/u;
623
+ const cacheMountTargets = (args) => [...args.matchAll(BUILDKIT_MOUNT_FLAG_RE)].map((match) => (match.groups?.spec ?? "").split(",")).filter((options) => options.includes("type=cache")).flatMap((options) => options.filter((option) => CACHE_TARGET_KEY_RE.test(option)).map((option) => option.slice(option.indexOf("=") + 1)));
624
+ const APT_CACHE_DIRS = ["/var/lib/apt", "/var/cache/apt"];
625
+ const APK_CACHE_DIRS = ["/var/cache/apk", "/etc/apk/cache"];
626
+ const hasCacheMountFor = (args, cacheDirs) => cacheMountTargets(args).some((target) => cacheDirs.some((dir) => target === dir || target.startsWith(`${dir}/`)));
595
627
  const cleanPackageCache = {
596
628
  category: "Image Size",
597
629
  check(instructions, file) {
598
630
  const diagnostics = [];
599
631
  for (const inst of instructions) if (inst.instruction === "RUN") {
600
632
  const { args } = inst;
601
- if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists")) diagnostics.push(createDiagnostic$2(file, this.key, this.defaultSeverity, `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`, this.help, inst.line));
602
- if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk")) diagnostics.push(createDiagnostic$2(file, this.key, this.defaultSeverity, `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`, this.help, inst.line));
633
+ if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists") && !hasCacheMountFor(args, APT_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`, this.help, inst.line));
634
+ if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk") && !hasCacheMountFor(args, APK_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`, this.help, inst.line));
603
635
  }
604
636
  return diagnostics;
605
637
  },
@@ -636,7 +668,7 @@ const avoidDevDependencies = {
636
668
  for (const inst of stage.runs) {
637
669
  if (!installsDevDependencies(inst.args)) continue;
638
670
  const where = stageIndex === finalIndex ? "in the final stage" : `in stage '${stage.name}', whose layers the final stage inherits,`;
639
- diagnostics.push(createDiagnostic$2(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
671
+ diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
640
672
  }
641
673
  }
642
674
  return diagnostics;
@@ -654,19 +686,11 @@ const imageSizeRules = [
654
686
 
655
687
  //#endregion
656
688
  //#region ../core/src/rules/performance.ts
657
- const createDiagnostic$1 = (file, ruleKey, severity, message, help, line) => ({
658
- file,
659
- help,
660
- line,
661
- message,
662
- rule: ruleKey,
663
- severity
664
- });
665
689
  const useMultiStage = {
666
690
  category: "Performance",
667
691
  check(instructions, file) {
668
692
  if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
669
- if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic$1(file, this.key, this.defaultSeverity, "Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.", this.help, instructions.find((inst) => inst.instruction === "FROM")?.line || 1)];
693
+ if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic(file, this.key, this.defaultSeverity, "Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.", this.help, instructions.find((inst) => inst.instruction === "FROM")?.line || 1)];
670
694
  }
671
695
  return [];
672
696
  },
@@ -690,7 +714,7 @@ const orderLayers = {
690
714
  }
691
715
  if (inst.instruction === "RUN" && copyAllLine !== -1) {
692
716
  const args = inst.args.toLowerCase();
693
- if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic$1(file, this.key, this.defaultSeverity, `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`, this.help, inst.line));
717
+ if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`, this.help, inst.line));
694
718
  }
695
719
  }
696
720
  return diagnostics;
@@ -710,10 +734,10 @@ const minimizeLayers = {
710
734
  if (consecutiveRunCount === 0) firstRunLine = inst.line;
711
735
  consecutiveRunCount += 1;
712
736
  } else {
713
- if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic$1(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
737
+ if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
714
738
  consecutiveRunCount = 0;
715
739
  }
716
- if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic$1(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
740
+ if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
717
741
  return diagnostics;
718
742
  },
719
743
  defaultSeverity: "info",
@@ -738,7 +762,7 @@ const useDockerignore = {
738
762
  return src === "." || src === "./" || src === "*";
739
763
  }
740
764
  return false;
741
- }) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic$1(file, this.key, this.defaultSeverity, "Using COPY/ADD with a wildcard or directory, but no .dockerignore file was found next to the Dockerfile or at the project root. This can copy local build folders and secrets.", this.help, 1)];
765
+ }) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic(file, this.key, this.defaultSeverity, "Using COPY/ADD with a wildcard or directory, but no .dockerignore file was found next to the Dockerfile or at the project root. This can copy local build folders and secrets.", this.help, 1)];
742
766
  return [];
743
767
  },
744
768
  defaultSeverity: "warning",
@@ -755,14 +779,6 @@ const performanceRules = [
755
779
 
756
780
  //#endregion
757
781
  //#region ../core/src/rules/security.ts
758
- const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
759
- file,
760
- help,
761
- line,
762
- message,
763
- rule: ruleKey,
764
- severity
765
- });
766
782
  const isRootUser = (value) => {
767
783
  const [user] = value.split(":");
768
784
  return user === "root" || user === "0";
@@ -909,12 +925,12 @@ const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categ
909
925
 
910
926
  //#endregion
911
927
  //#region ../core/src/runners/compose-runner.ts
912
- const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig) => {
928
+ const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, locate) => {
913
929
  const diagnostics = [];
914
930
  for (const rule of allComposeRules) {
915
931
  const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
916
932
  if (severity === "off") continue;
917
- const ruleDiagnostics = rule.check(composeContent, file);
933
+ const ruleDiagnostics = rule.check(composeContent, file, { locate });
918
934
  if (severity !== rule.defaultSeverity) for (const diag of ruleDiagnostics) diag.severity = severity;
919
935
  diagnostics.push(...ruleDiagnostics);
920
936
  }
@@ -1163,6 +1179,12 @@ Object.defineProperty(exports, 'calculateScore', {
1163
1179
  return calculateScore;
1164
1180
  }
1165
1181
  });
1182
+ Object.defineProperty(exports, 'createComposeLocator', {
1183
+ enumerable: true,
1184
+ get: function () {
1185
+ return createComposeLocator;
1186
+ }
1187
+ });
1166
1188
  Object.defineProperty(exports, 'discoverProject', {
1167
1189
  enumerable: true,
1168
1190
  get: function () {
@@ -1217,4 +1239,4 @@ Object.defineProperty(exports, 'version', {
1217
1239
  return version;
1218
1240
  }
1219
1241
  });
1220
- //# sourceMappingURL=src-ILbJuJmE.cjs.map
1242
+ //# sourceMappingURL=src-DwuAaQcq.cjs.map