cspell 10.3.5 → 10.4.0-alpha.1

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.
@@ -24,7 +24,7 @@ import { dynamicImport } from "@cspell/dynamic-import";
24
24
  import crypto from "node:crypto";
25
25
  import streamConsumers from "node:stream/consumers";
26
26
  import { getStat, readFileText, toURL } from "cspell-io";
27
- import { glob } from "tinyglobby";
27
+ import { glob, isDynamicPattern } from "tinyglobby";
28
28
  import * as readline from "node:readline";
29
29
  import { parse, stringify } from "flatted";
30
30
  //#region src/console.ts
@@ -119,6 +119,107 @@ function toApplicationError(e, message) {
119
119
  return new ApplicationError(message ?? err.message, void 0, err);
120
120
  }
121
121
  //#endregion
122
+ //#region src/formatIssue.ts
123
+ function formatIssue(io, templateStr, issue, maxIssueTextWidth) {
124
+ function clean(t) {
125
+ return t.replace(/\s+/, " ");
126
+ }
127
+ const { uri = "", filename, row, col, text, context = issue.line, offset } = issue;
128
+ const textLen = issue.length ?? text.length;
129
+ const contextLeft = clean(context.text.slice(0, offset - context.offset));
130
+ const contextRight = clean(context.text.slice(offset + textLen - context.offset));
131
+ const contextFull = clean(context.text);
132
+ const padContext = " ".repeat(Math.max(maxIssueTextWidth - text.length, 0));
133
+ const rowText = row.toString();
134
+ const colText = col.toString();
135
+ const padRowCol = " ".repeat(Math.max(1, 8 - (rowText.length + colText.length)));
136
+ const suggestions = formatSuggestions(io, issue);
137
+ const msg = issue.message || (issue.isFlagged ? "Forbidden word" : "Unknown word");
138
+ const messageColored = issue.isFlagged ? `{yellow ${msg}}` : msg;
139
+ const substitutions = {
140
+ $col: colText,
141
+ $contextFull: contextFull,
142
+ $contextLeft: contextLeft,
143
+ $contextRight: contextRight,
144
+ $filename: filename,
145
+ $padContext: padContext,
146
+ $padRowCol: padRowCol,
147
+ $row: rowText,
148
+ $suggestions: suggestions,
149
+ $text: text,
150
+ $uri: uri,
151
+ $quickFix: formatQuickFix(io, issue),
152
+ $message: msg,
153
+ $messageColored: messageColored
154
+ };
155
+ const t = templateStr.replaceAll("$messageColored", messageColored);
156
+ return substitute(makeTemplate(io.chalk)(t), substitutions).trimEnd();
157
+ }
158
+ function formatSuggestions(io, issue) {
159
+ if (issue.suggestionsEx) return issue.suggestionsEx.map((sug) => sug.isPreferred ? io.chalk.italic(io.chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + "*" : sug.wordAdjustedToMatchCase || sug.word).join(", ");
160
+ if (issue.suggestions) return issue.suggestions.join(", ");
161
+ return "";
162
+ }
163
+ function formatQuickFix(io, issue) {
164
+ if (!issue.suggestionsEx?.length) return "";
165
+ const preferred = issue.suggestionsEx.filter((sug) => sug.isPreferred).map((sug) => sug.wordAdjustedToMatchCase || sug.word);
166
+ if (!preferred.length) return "";
167
+ return `fix: (${preferred.map((w) => io.chalk.italic(io.chalk.yellow(w))).join(", ")})`;
168
+ }
169
+ function substitute(text, substitutions) {
170
+ const subs = [];
171
+ for (const [match, replaceWith] of Object.entries(substitutions)) {
172
+ const len = match.length;
173
+ for (let i = text.indexOf(match); i >= 0; i = text.indexOf(match, i)) {
174
+ const end = i + len;
175
+ const reg = /\b/y;
176
+ reg.lastIndex = end;
177
+ if (reg.test(text)) subs.push([
178
+ i,
179
+ end,
180
+ replaceWith
181
+ ]);
182
+ i = end;
183
+ }
184
+ }
185
+ subs.sort((a, b) => a[0] - b[0]);
186
+ let i = 0;
187
+ function sub(r) {
188
+ const [a, b, t] = r;
189
+ const prefix = text.slice(i, a);
190
+ i = b;
191
+ return prefix + t;
192
+ }
193
+ return subs.map(sub).join("") + text.slice(i);
194
+ }
195
+ function checkTemplate(template) {
196
+ const chalk = new Chalk();
197
+ const chalkTemplate = makeTemplate(chalk);
198
+ const substitutions = {
199
+ $col: "<col>",
200
+ $contextFull: "<contextFull>",
201
+ $contextLeft: "<contextLeft>",
202
+ $contextRight: "<contextRight>",
203
+ $filename: "<filename>",
204
+ $padContext: "<padContext>",
205
+ $padRowCol: "<padRowCol>",
206
+ $row: "<row>",
207
+ $suggestions: "<suggestions>",
208
+ $text: "<text>",
209
+ $uri: "<uri>",
210
+ $quickFix: "<quickFix>",
211
+ $message: "<message>",
212
+ $messageColored: "<messageColored>"
213
+ };
214
+ try {
215
+ const problems = [...substitute(chalkTemplate(template), substitutions).matchAll(/\$[a-z]+/gi)].map((m) => m[0]);
216
+ if (problems.length) throw new Error(`Unresolved template variable${problems.length > 1 ? "s" : ""}: ${problems.map((v) => `'${v}'`).join(", ")}`);
217
+ return true;
218
+ } catch (e) {
219
+ return new ApplicationError(e instanceof Error ? e.message : `${e}`);
220
+ }
221
+ }
222
+ //#endregion
122
223
  //#region src/util/perfMeasurements.ts
123
224
  function getPerfMeasurements() {
124
225
  const measurements = performance.getEntriesByType("measure");
@@ -563,6 +664,9 @@ function getReporter(options, config) {
563
664
  filesCached: 0,
564
665
  accumulatedTimeMs: 0,
565
666
  startTime: performance.now(),
667
+ firstFileStartTime: 0,
668
+ lastFileEndTime: 0,
669
+ betweenFilesTimeMs: 0,
566
670
  perf: Object.create(null)
567
671
  };
568
672
  const noColor = options.color === false;
@@ -637,13 +741,19 @@ function getReporter(options, config) {
637
741
  errorCollection.forEach((error) => consoleError(error));
638
742
  }
639
743
  if (options.showPerfSummary) {
640
- const elapsedTotal = performance.now() - perfStats.startTime;
744
+ const now = performance.now();
745
+ const elapsedTotal = now - perfStats.startTime;
746
+ const setupTime = (perfStats.firstFileStartTime || now) - perfStats.startTime;
747
+ const otherTime = elapsedTotal - setupTime - perfStats.accumulatedTimeMs - perfStats.betweenFilesTimeMs;
641
748
  consoleError("-------------------------------------------");
642
749
  consoleError("Performance Summary:");
643
750
  consoleError(` Files Processed : ${perfStats.filesProcessed.toString().padStart(11)}`);
644
751
  consoleError(` Files Skipped : ${perfStats.filesSkipped.toString().padStart(11)}`);
645
752
  consoleError(` Files Cached : ${perfStats.filesCached.toString().padStart(11)}`);
753
+ consoleError(` Setup Time : ${setupTime.toFixed(2).padStart(9)}ms`);
646
754
  consoleError(` Processing Time : ${perfStats.accumulatedTimeMs.toFixed(2).padStart(9)}ms`);
755
+ consoleError(` Between Files : ${perfStats.betweenFilesTimeMs.toFixed(2).padStart(9)}ms`);
756
+ consoleError(` Other Time : ${otherTime.toFixed(2).padStart(9)}ms`);
647
757
  consoleError(` Total Time : ${elapsedTotal.toFixed(2).padStart(9)}ms`);
648
758
  const tableStats = {
649
759
  title: chalk.bold("Perf Stats:"),
@@ -755,7 +865,15 @@ function getReporter(options, config) {
755
865
  }
756
866
  function progress(p) {
757
867
  if (!silent && showProgress) reportProgress(stderr, p, rootURL, options);
758
- if (p.type === "ProgressFileComplete") collectPerfStats(p);
868
+ if (p.type === "ProgressFileBegin") {
869
+ const now = performance.now();
870
+ perfStats.firstFileStartTime ||= now;
871
+ if (perfStats.lastFileEndTime) perfStats.betweenFilesTimeMs += now - perfStats.lastFileEndTime;
872
+ }
873
+ if (p.type === "ProgressFileComplete") {
874
+ perfStats.lastFileEndTime = performance.now();
875
+ collectPerfStats(p);
876
+ }
759
877
  }
760
878
  return {
761
879
  issue: relativeIssue(silent || !issues ? nullEmitter : genIssueEmitter(stdio, stderr, issueTemplate, uniqueIssues, issuesCollection)),
@@ -764,7 +882,7 @@ function getReporter(options, config) {
764
882
  debug: emitters.Debug,
765
883
  progress,
766
884
  result: !silent && summary ? resultEmitter : nullEmitter,
767
- features: void 0
885
+ features: { issueType: true }
768
886
  };
769
887
  }
770
888
  function extractNotableBySelfTimeInGroup(measurements) {
@@ -788,109 +906,10 @@ function extractNotableBySelfTimeInGroup(measurements) {
788
906
  if (highest) notable.add(highest);
789
907
  return notable;
790
908
  }
791
- function formatIssue(io, templateStr, issue, maxIssueTextWidth) {
792
- function clean(t) {
793
- return t.replace(/\s+/, " ");
794
- }
795
- const { uri = "", filename, row, col, text, context = issue.line, offset } = issue;
796
- const textLen = issue.length ?? text.length;
797
- const contextLeft = clean(context.text.slice(0, offset - context.offset));
798
- const contextRight = clean(context.text.slice(offset + textLen - context.offset));
799
- const contextFull = clean(context.text);
800
- const padContext = " ".repeat(Math.max(maxIssueTextWidth - text.length, 0));
801
- const rowText = row.toString();
802
- const colText = col.toString();
803
- const padRowCol = " ".repeat(Math.max(1, 8 - (rowText.length + colText.length)));
804
- const suggestions = formatSuggestions(io, issue);
805
- const msg = issue.message || (issue.isFlagged ? "Forbidden word" : "Unknown word");
806
- const messageColored = issue.isFlagged ? `{yellow ${msg}}` : msg;
807
- const substitutions = {
808
- $col: colText,
809
- $contextFull: contextFull,
810
- $contextLeft: contextLeft,
811
- $contextRight: contextRight,
812
- $filename: filename,
813
- $padContext: padContext,
814
- $padRowCol: padRowCol,
815
- $row: rowText,
816
- $suggestions: suggestions,
817
- $text: text,
818
- $uri: uri,
819
- $quickFix: formatQuickFix(io, issue),
820
- $message: msg,
821
- $messageColored: messageColored
822
- };
823
- const t = templateStr.replaceAll("$messageColored", messageColored);
824
- return substitute(makeTemplate(io.chalk)(t), substitutions).trimEnd();
825
- }
826
- function formatSuggestions(io, issue) {
827
- if (issue.suggestionsEx) return issue.suggestionsEx.map((sug) => sug.isPreferred ? io.chalk.italic(io.chalk.bold(sug.wordAdjustedToMatchCase || sug.word)) + "*" : sug.wordAdjustedToMatchCase || sug.word).join(", ");
828
- if (issue.suggestions) return issue.suggestions.join(", ");
829
- return "";
830
- }
831
- function formatQuickFix(io, issue) {
832
- if (!issue.suggestionsEx?.length) return "";
833
- const preferred = issue.suggestionsEx.filter((sug) => sug.isPreferred).map((sug) => sug.wordAdjustedToMatchCase || sug.word);
834
- if (!preferred.length) return "";
835
- return `fix: (${preferred.map((w) => io.chalk.italic(io.chalk.yellow(w))).join(", ")})`;
836
- }
837
- function substitute(text, substitutions) {
838
- const subs = [];
839
- for (const [match, replaceWith] of Object.entries(substitutions)) {
840
- const len = match.length;
841
- for (let i = text.indexOf(match); i >= 0; i = text.indexOf(match, i)) {
842
- const end = i + len;
843
- const reg = /\b/y;
844
- reg.lastIndex = end;
845
- if (reg.test(text)) subs.push([
846
- i,
847
- end,
848
- replaceWith
849
- ]);
850
- i = end;
851
- }
852
- }
853
- subs.sort((a, b) => a[0] - b[0]);
854
- let i = 0;
855
- function sub(r) {
856
- const [a, b, t] = r;
857
- const prefix = text.slice(i, a);
858
- i = b;
859
- return prefix + t;
860
- }
861
- return subs.map(sub).join("") + text.slice(i);
862
- }
863
909
  function assertCheckTemplate(template) {
864
910
  const r = checkTemplate(template);
865
911
  if (r instanceof Error) throw r;
866
912
  }
867
- function checkTemplate(template) {
868
- const chalk = new Chalk();
869
- const chalkTemplate = makeTemplate(chalk);
870
- const substitutions = {
871
- $col: "<col>",
872
- $contextFull: "<contextFull>",
873
- $contextLeft: "<contextLeft>",
874
- $contextRight: "<contextRight>",
875
- $filename: "<filename>",
876
- $padContext: "<padContext>",
877
- $padRowCol: "<padRowCol>",
878
- $row: "<row>",
879
- $suggestions: "<suggestions>",
880
- $text: "<text>",
881
- $uri: "<uri>",
882
- $quickFix: "<quickFix>",
883
- $message: "<message>",
884
- $messageColored: "<messageColored>"
885
- };
886
- try {
887
- const problems = [...substitute(chalkTemplate(template), substitutions).matchAll(/\$[a-z]+/gi)].map((m) => m[0]);
888
- if (problems.length) throw new Error(`Unresolved template variable${problems.length > 1 ? "s" : ""}: ${problems.map((v) => `'${v}'`).join(", ")}`);
889
- return true;
890
- } catch (e) {
891
- return new ApplicationError(e instanceof Error ? e.message : `${e}`);
892
- }
893
- }
894
913
  //#endregion
895
914
  //#region src/config/adjustConfig.ts
896
915
  async function fileExists(url) {
@@ -1183,7 +1202,9 @@ const environmentKeys = {
1183
1202
  CSPELL_ENABLE_DICTIONARY_LOG_FIELDS: "CSPELL_ENABLE_DICTIONARY_LOG_FIELDS",
1184
1203
  CSPELL_GLOB_ROOT: "CSPELL_GLOB_ROOT",
1185
1204
  CSPELL_CONFIG_PATH: "CSPELL_CONFIG_PATH",
1186
- CSPELL_DEFAULT_CONFIG_PATH: "CSPELL_DEFAULT_CONFIG_PATH"
1205
+ CSPELL_DEFAULT_CONFIG_PATH: "CSPELL_DEFAULT_CONFIG_PATH",
1206
+ CSPELL_FOLLOW_SYMLINKS: "CSPELL_FOLLOW_SYMLINKS",
1207
+ CSPELL_GLOB_SYMLINKS: "CSPELL_GLOB_SYMLINKS"
1187
1208
  };
1188
1209
  function setEnvironmentVariable(key, value) {
1189
1210
  process.env[key] = value;
@@ -1213,7 +1234,7 @@ try {
1213
1234
  const pkgDir = _dirname;
1214
1235
  const npmPackage = {
1215
1236
  name: "cspell",
1216
- version: "10.3.5",
1237
+ version: "10.4.0-alpha.1",
1217
1238
  engines: { node: ">=22.18.0" }
1218
1239
  };
1219
1240
  //#endregion
@@ -1351,7 +1372,7 @@ var LintReporter = class {
1351
1372
  });
1352
1373
  }
1353
1374
  emitProgressComplete(filename, fileNum, fileCount, result) {
1354
- const numIssues = result.issues.filter((issue) => filterFeatureIssues({}, issue, result.reportIssueOptions)).length;
1375
+ const numIssues = result.issues.filter((issue) => filterFeatureIssues({ issueType: true }, issue, result.reportIssueOptions)).length;
1355
1376
  for (const reporter of this.#reporters) {
1356
1377
  const progress = clean({
1357
1378
  type: "ProgressFileComplete",
@@ -1448,7 +1469,7 @@ async function globP(pattern, options) {
1448
1469
  dot,
1449
1470
  ignore,
1450
1471
  absolute: true,
1451
- followSymbolicLinks: false,
1472
+ followSymbolicLinks: options?.followSymlinks ?? false,
1452
1473
  expandDirectories: false
1453
1474
  });
1454
1475
  const compare = new Intl.Collator("en").compare;
@@ -1992,18 +2013,20 @@ var DiskCache = class {
1992
2013
  cspellVersion;
1993
2014
  useUniversalCache;
1994
2015
  fileEntryCache;
2016
+ optionsKey;
1995
2017
  cacheDir;
1996
2018
  dependencyCache = /* @__PURE__ */ new Map();
1997
2019
  dependencyCacheTree = {};
1998
2020
  objectCollection = new ShallowObjectCollection();
1999
2021
  ocCacheFileResult = new ShallowObjectCollection();
2000
2022
  version;
2001
- constructor(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache, fileEntryCache) {
2023
+ constructor(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache, fileEntryCache, optionsKey) {
2002
2024
  this.cacheFileLocation = cacheFileLocation;
2003
2025
  this.useCheckSum = useCheckSum;
2004
2026
  this.cspellVersion = cspellVersion;
2005
2027
  this.useUniversalCache = useUniversalCache;
2006
2028
  this.fileEntryCache = fileEntryCache;
2029
+ this.optionsKey = optionsKey;
2007
2030
  this.cacheDir = fileURLToPath(new URL("./", cacheFileLocation));
2008
2031
  this.version = calcVersion(cspellVersion);
2009
2032
  }
@@ -2014,7 +2037,8 @@ var DiskCache = class {
2014
2037
  const data = meta?.data;
2015
2038
  const result = data?.r;
2016
2039
  const versionMatches = this.version === data?.v;
2017
- if (fileDescriptor.notFound || fileDescriptor.changed || !meta || !result || !versionMatches || !await this.checkDependencies(data.d)) return;
2040
+ const optionsMatch = this.optionsKey === data?.o;
2041
+ if (fileDescriptor.notFound || fileDescriptor.changed || !meta || !result || !versionMatches || !optionsMatch || !await this.checkDependencies(data.d)) return;
2018
2042
  const dd = { ...data };
2019
2043
  if (dd.d) dd.d = setTreeEntry(this.dependencyCacheTree, dd.d);
2020
2044
  dd.r = dd.r && this.normalizeResult(dd.r);
@@ -2036,7 +2060,8 @@ var DiskCache = class {
2036
2060
  meta.data = this.objectCollection.get({
2037
2061
  v: this.version,
2038
2062
  r: this.normalizeResult(result),
2039
- d: await this.calcDependencyHashes(dependsUponFiles)
2063
+ d: await this.calcDependencyHashes(dependsUponFiles),
2064
+ ...this.optionsKey ? { o: this.optionsKey } : {}
2040
2065
  });
2041
2066
  }
2042
2067
  async reconcile() {
@@ -2138,8 +2163,8 @@ async function getDependencyForUrl(remoteUrl) {
2138
2163
  };
2139
2164
  }
2140
2165
  }
2141
- async function createDiskCache(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache) {
2142
- return new DiskCache(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache, await createFromFile(cacheFileLocation, useCheckSum, useUniversalCache));
2166
+ async function createDiskCache(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache, optionsKey) {
2167
+ return new DiskCache(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache, await createFromFile(cacheFileLocation, useCheckSum, useUniversalCache), optionsKey);
2143
2168
  }
2144
2169
  function getTreeEntry(tree, keys) {
2145
2170
  let r = tree;
@@ -2208,7 +2233,7 @@ async function createCache(options) {
2208
2233
  const useChecksum = cacheStrategy === "content";
2209
2234
  const version = normalizeVersion(options.version);
2210
2235
  const useUniversal = options.cacheFormat === "universal";
2211
- const cache = useCache ? await createDiskCache(location, useChecksum, version, useUniversal) : new DummyCache();
2236
+ const cache = useCache ? await createDiskCache(location, useChecksum, version, useUniversal, options.optionsKey) : new DummyCache();
2212
2237
  if (reset) await cache.reset();
2213
2238
  return cache;
2214
2239
  }
@@ -2220,6 +2245,8 @@ async function calcCacheSettings(config, cacheOptions, root) {
2220
2245
  const cacheFormat = cacheOptions.cacheFormat ?? cs.cacheFormat ?? "universal";
2221
2246
  const optionals = {};
2222
2247
  if (cacheOptions.cacheReset) optionals.reset = true;
2248
+ const optionsKey = calcOptionsKey(cacheOptions);
2249
+ if (optionsKey) optionals.optionsKey = optionsKey;
2223
2250
  return {
2224
2251
  ...optionals,
2225
2252
  useCache,
@@ -2229,6 +2256,17 @@ async function calcCacheSettings(config, cacheOptions, root) {
2229
2256
  cacheFormat
2230
2257
  };
2231
2258
  }
2259
+ function calcOptionsKey(cacheOptions) {
2260
+ const { languageId, locale, report, showSuggestions, validateDirectives } = cacheOptions;
2261
+ const set = Object.entries({
2262
+ languageId: languageId || void 0,
2263
+ locale: locale || void 0,
2264
+ report: report || void 0,
2265
+ showSuggestions,
2266
+ validateDirectives
2267
+ }).filter(([, value]) => value !== void 0);
2268
+ return set.length ? JSON.stringify(Object.fromEntries(set)) : void 0;
2269
+ }
2232
2270
  async function resolveCacheLocation(cacheLocation) {
2233
2271
  try {
2234
2272
  if ((await stat(cacheLocation)).isFile()) return cacheLocation;
@@ -2288,6 +2326,48 @@ async function readConfigHandleError(filename) {
2288
2326
  }
2289
2327
  }
2290
2328
  //#endregion
2329
+ //#region src/util/symlinks.ts
2330
+ function createSymlinkChecker(root) {
2331
+ const rootPath = path$1.resolve(root);
2332
+ const cache = /* @__PURE__ */ new Map();
2333
+ function isSymlink(filename) {
2334
+ let found = cache.get(filename);
2335
+ if (!found) {
2336
+ found = promises.lstat(filename).then((s) => s.isSymbolicLink(), () => false);
2337
+ cache.set(filename, found);
2338
+ }
2339
+ return found;
2340
+ }
2341
+ async function isReachedThroughSymlink(filename) {
2342
+ if (filename === "stdin" || isStdinUrl(filename)) return false;
2343
+ const absFilename = path$1.resolve(rootPath, filename);
2344
+ const rel = path$1.relative(rootPath, absFilename);
2345
+ if (!rel) return false;
2346
+ if (rel === ".." || rel.startsWith(".." + path$1.sep) || path$1.isAbsolute(rel)) return isSymlink(absFilename);
2347
+ let current = rootPath;
2348
+ for (const segment of rel.split(path$1.sep)) {
2349
+ current = path$1.join(current, segment);
2350
+ if (await isSymlink(current)) return true;
2351
+ }
2352
+ return false;
2353
+ }
2354
+ async function isGlobReachedThroughSymlink(pattern) {
2355
+ const segments = pattern.split("/");
2356
+ const fixed = [];
2357
+ for (const segment of segments) {
2358
+ if (isDynamicPattern(segment)) break;
2359
+ fixed.push(segment);
2360
+ }
2361
+ const base = fixed.join("/");
2362
+ if (!base) return false;
2363
+ return isReachedThroughSymlink(path$1.resolve(rootPath, base));
2364
+ }
2365
+ return {
2366
+ isReachedThroughSymlink,
2367
+ isGlobReachedThroughSymlink
2368
+ };
2369
+ }
2370
+ //#endregion
2291
2371
  //#region src/util/timer.ts
2292
2372
  function getTimeMeasurer() {
2293
2373
  const timer = createPerfTimer("timer");
@@ -2687,13 +2767,30 @@ function prefetch(fileToProcess, cfg) {
2687
2767
  const reportIssueOptions = extractReporterIssueOptions(cfg.config);
2688
2768
  async function fetch() {
2689
2769
  const getElapsedTimeMs = getTimeMeasurer();
2770
+ if (!cfg.followSymlinks && await cfg.symlinks.isReachedThroughSymlink(filename)) {
2771
+ const skipReason = "referenced by a symbolic link, use --follow-symlinks to check it";
2772
+ if (cfg.forceCheck) return {
2773
+ skip: true,
2774
+ skipReason,
2775
+ error: new LinterError(`File ${skipReason}: "${filename}"`)
2776
+ };
2777
+ return {
2778
+ skip: true,
2779
+ skipReason: cfg.verbose ? skipReason : void 0
2780
+ };
2781
+ }
2690
2782
  const cachedResult = await cfg.cache.getCachedLintResults(filename);
2691
2783
  if (cachedResult) return { fileResult: {
2692
2784
  ...cachedResult,
2693
2785
  elapsedTimeMs: getElapsedTimeMs()
2694
2786
  } };
2695
2787
  const uri = filenameToUri(filename, cfg.root).href;
2696
- const checkResult = await shouldCheckDocument({ uri }, { forceCheck: cfg.forceCheck }, cfg.config);
2788
+ const { languageId, locale } = cfg;
2789
+ const checkResult = await shouldCheckDocument({
2790
+ uri,
2791
+ languageId,
2792
+ locale
2793
+ }, { forceCheck: cfg.forceCheck }, cfg.config);
2697
2794
  if (!checkResult.shouldCheck) return {
2698
2795
  skip: true,
2699
2796
  skipReason: checkResult.reason || "Ignored by configuration."
@@ -2725,6 +2822,11 @@ async function processFiles(files, options) {
2725
2822
  root: options.cfg.root,
2726
2823
  maxFileSize: options.cfg.maxFileSize,
2727
2824
  forceCheck: !!options.cfg.options.forceCheck,
2825
+ symlinks: options.symlinks ?? createSymlinkChecker(options.cfg.root),
2826
+ followSymlinks: !!options.followSymlinks,
2827
+ verbose: options.verboseLevel > 0,
2828
+ languageId: options.cfg.options.languageId || void 0,
2829
+ locale: options.cfg.locale || void 0,
2728
2830
  config: options.configInfo.config,
2729
2831
  cache
2730
2832
  };
@@ -2760,17 +2862,24 @@ async function processFiles(files, options) {
2760
2862
  if (fetchResult instanceof Error) throw fetchResult;
2761
2863
  const fileNum = sequence + 1;
2762
2864
  reporter.emitProgressBegin(filename, fileNum, pf.sequenceSize ?? sequence);
2763
- if (fetchResult?.skip) return {
2764
- filename,
2765
- sequence,
2766
- sequenceSize,
2767
- result: {
2865
+ if (fetchResult?.skip) {
2866
+ const result = {
2768
2867
  ...emptyResult,
2769
2868
  fileInfo: { filename },
2770
2869
  elapsedTimeMs: getElapsedTimeMs(),
2771
2870
  skippedReason: fetchResult.skipReason
2871
+ };
2872
+ if (fetchResult.error) {
2873
+ new ReportItemCollector(result).error("Linter:", fetchResult.error);
2874
+ result.errors = 1;
2772
2875
  }
2773
- };
2876
+ return {
2877
+ filename,
2878
+ sequence,
2879
+ sequenceSize,
2880
+ result
2881
+ };
2882
+ }
2774
2883
  return {
2775
2884
  filename,
2776
2885
  sequence,
@@ -2826,7 +2935,7 @@ function runResult(init = {}) {
2826
2935
  };
2827
2936
  }
2828
2937
  //#endregion
2829
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/usingCtx.js
2938
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/usingCtx.js
2830
2939
  function _usingCtx() {
2831
2940
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
2832
2941
  var n = Error();
@@ -2935,7 +3044,8 @@ async function runLint(cfg) {
2935
3044
  ...cfg.options,
2936
3045
  version
2937
3046
  }, root);
2938
- const filesToCheck = await determineFilesToCheck(configInfo, cfg, reporter, globInfo);
3047
+ const symlinks = createSymlinkChecker(root);
3048
+ const filesToCheck = await determineFilesToCheck(configInfo, cfg, reporter, globInfo, symlinks);
2939
3049
  const processFilesOptions = {
2940
3050
  chalk,
2941
3051
  configInfo,
@@ -2945,7 +3055,9 @@ async function runLint(cfg) {
2945
3055
  configErrors,
2946
3056
  userSettings: configInfo.config,
2947
3057
  lintReporter: reporter,
2948
- cacheSettings
3058
+ cacheSettings,
3059
+ symlinks,
3060
+ followSymlinks: filesToCheck.followSymlinks
2949
3061
  };
2950
3062
  const result = await processFiles(filesToCheck.files, processFilesOptions);
2951
3063
  if (configErrorCount && cfg.options.exitCode !== false) result.errors ||= configErrorCount;
@@ -3026,10 +3138,11 @@ function filesToProcess(files) {
3026
3138
  sequenceSize
3027
3139
  }));
3028
3140
  }
3029
- async function determineFilesToCheck(configInfo, cfg, reporter, globInfo) {
3141
+ async function determineFilesToCheck(configInfo, cfg, reporter, globInfo, symlinks) {
3030
3142
  const result = {
3031
3143
  files: [],
3032
- numFiles: 0
3144
+ numFiles: 0,
3145
+ followSymlinks: false
3033
3146
  };
3034
3147
  function countFiles(fnKeep) {
3035
3148
  if (!fnKeep) return () => {
@@ -3047,6 +3160,13 @@ async function determineFilesToCheck(configInfo, cfg, reporter, globInfo) {
3047
3160
  async function determineFilesToCheckFromCliFiles() {
3048
3161
  const { fileLists } = cfg;
3049
3162
  const hasFileLists = !!fileLists.length;
3163
+ const followSymlinks = cfg.options.followSymlinks ?? truthy(getEnvironmentVariable("CSPELL_FOLLOW_SYMLINKS"));
3164
+ result.followSymlinks = followSymlinks;
3165
+ const keepSymlinks = (fn) => followSymlinks ? fn : async (file) => await symlinks.isReachedThroughSymlink(file) || fn(file);
3166
+ if (!followSymlinks) for (const fileList of fileLists) {
3167
+ if (fileList === "stdin") continue;
3168
+ if (await symlinks.isReachedThroughSymlink(path$1.resolve(fileList))) throw new ApplicationError(`Error reading file list from: "${fileList}". It is referenced by a symbolic link, use --follow-symlinks to read it.`);
3169
+ }
3050
3170
  const { gitIgnore, allGlobs, excludeGlobs, normalizedExcludes } = globInfo;
3051
3171
  const { root } = cfg;
3052
3172
  const globsToExcludeRaw = [...configInfo.config.ignorePaths || [], ...excludeGlobs];
@@ -3066,8 +3186,8 @@ async function determineFilesToCheck(configInfo, cfg, reporter, globInfo) {
3066
3186
  if (enableGlobDot !== void 0) globOptions.dot = enableGlobDot;
3067
3187
  const includeFilterCountSkipped = countFiles(cfg.options.forceCheck && !cfg.fileGlobs.length ? () => true : createIncludeFileFilterFn(allGlobs, root, enableGlobDot));
3068
3188
  const rawCliFiles = cfg.files?.map((file) => resolveFilename(file, root)).filter(includeFilterCountSkipped);
3069
- const cliFiles = cfg.options.mustFindFiles ? rawCliFiles : rawCliFiles && pipeAsync(rawCliFiles, opFilterAsync(isFile));
3070
- const foundFiles = hasFileLists ? concatAsyncIterables(cliFiles, await useFileLists(fileLists, includeFilterCountSkipped)) : cliFiles || [];
3189
+ const cliFiles = cfg.options.mustFindFiles ? rawCliFiles : rawCliFiles && pipeAsync(rawCliFiles, opFilterAsync(keepSymlinks(isFile)));
3190
+ const foundFiles = hasFileLists ? concatAsyncIterables(cliFiles, await useFileLists(fileLists, includeFilterCountSkipped, keepSymlinks(isNotDir))) : cliFiles || [];
3071
3191
  const opFilterExcludedFiles = opFilter(filterOutExcludedFilesFn(globMatcher));
3072
3192
  const filtered = !cfg.options.forceCheck && gitIgnore ? await gitIgnore.filterOutIgnored(foundFiles) : foundFiles;
3073
3193
  return isAsyncIterable(filtered) ? pipeAsync(filtered, opFilterExcludedFiles, filesToProcessAsync) : filesToProcess(pipe(filtered, opFilterExcludedFiles));
@@ -3090,8 +3210,19 @@ async function determineFilesToCheck(configInfo, cfg, reporter, globInfo) {
3090
3210
  };
3091
3211
  const enableGlobDot = cfg.enableGlobDot ?? configInfo.config.enableGlobDot;
3092
3212
  if (enableGlobDot !== void 0) globOptions.dot = enableGlobDot;
3213
+ const globSymlinks = cfg.options.globSymlinks ?? truthy(getEnvironmentVariable("CSPELL_GLOB_SYMLINKS"));
3214
+ globOptions.followSymlinks = globSymlinks;
3215
+ result.followSymlinks = globSymlinks;
3093
3216
  const opFilterExcludedFiles = opFilter(filterOutExcludedFilesFn(globMatcher));
3094
- const foundFiles = await findFiles(fileGlobs, globOptions);
3217
+ const globsToSearch = [];
3218
+ for (const glob of fileGlobs) {
3219
+ if (!globSymlinks && await symlinks.isGlobReachedThroughSymlink(glob)) {
3220
+ if (calcVerboseLevel(cfg.options) > 0) reporter.info(`Glob not searched: "${glob}" goes through a symbolic link.`, MessageTypes$1.Info);
3221
+ continue;
3222
+ }
3223
+ globsToSearch.push(glob);
3224
+ }
3225
+ const foundFiles = await findFiles(globsToSearch, globOptions);
3095
3226
  const filtered = gitIgnore ? await gitIgnore.filterOutIgnored(foundFiles) : foundFiles;
3096
3227
  return isAsyncIterable(filtered) ? pipeAsync(filtered, opFilterExcludedFiles, filesToProcessAsync, opFilter(countFiles())) : filesToProcess(pipe(filtered, opFilterExcludedFiles)).filter(countFiles());
3097
3228
  }
@@ -3158,9 +3289,9 @@ async function generateGitIgnore(roots) {
3158
3289
  }
3159
3290
  return new GitIgnore(root?.map((p) => path$1.resolve(p)));
3160
3291
  }
3161
- async function useFileLists(fileListFiles, filterFiles) {
3292
+ async function useFileLists(fileListFiles, filterFiles, filterNotDir) {
3162
3293
  const files = readFileListFiles(fileListFiles);
3163
- return pipeAsync(files, opFilter(filterFiles), opFilterAsync(isNotDir));
3294
+ return pipeAsync(files, opFilter(filterFiles), opFilterAsync(filterNotDir));
3164
3295
  }
3165
3296
  function createIncludeFileFilterFn(includeGlobPatterns, root, dot) {
3166
3297
  if (!includeGlobPatterns?.length) return () => true;
@@ -3227,6 +3358,7 @@ var LintRequest = class {
3227
3358
  }];
3228
3359
  this.cspellSettingsFromCliOptions = {
3229
3360
  ...noConfigSearch !== void 0 ? { noConfigSearch } : {},
3361
+ ...options.validateDirectives !== void 0 ? { validateDirectives: options.validateDirectives } : {},
3230
3362
  ...extractUnknownWordsConfig(options),
3231
3363
  languageSettings
3232
3364
  };
@@ -3534,4 +3666,4 @@ function parseApplicationFeatureFlags(flags) {
3534
3666
  //#endregion
3535
3667
  export { ApplicationError as C, width as S, console as T, padLeft as _, parseApplicationFeatureFlags as a, pruneAnsiTextEnd as b, listDictionaries as c, validateUnitSize as d, unindent as f, tableToLines as g, getReporter as h, lint as i, ReportChoicesAll as l, npmPackage as m, checkText as n, suggestions as o, DEFAULT_CACHE_LOCATION as p, createInit as r, trace as s, IncludeExcludeFlag as t, cvtLinterCliCommandOptionsToLinterCliOptions as u, padWidth as v, CheckFailed as w, pruneAnsiTextStart as x, ansiWidth as y };
3536
3668
 
3537
- //# sourceMappingURL=application-CdcDwTFc.js.map
3669
+ //# sourceMappingURL=application-DRa52DKq.js.map
@@ -1,2 +1,2 @@
1
- import { a as checkText, c as parseApplicationFeatureFlags, d as listDictionaries, i as TraceResult, l as suggestions, n as CheckTextResult, o as createInit, r as IncludeExcludeFlag, s as lint, t as AppError, u as trace } from "./application-094uTsvC.js";
1
+ import { a as checkText, c as parseApplicationFeatureFlags, d as listDictionaries, i as TraceResult, l as suggestions, n as CheckTextResult, o as createInit, r as IncludeExcludeFlag, s as lint, t as AppError, u as trace } from "./application-CFfMlhE_.js";
2
2
  export { AppError, CheckTextResult, IncludeExcludeFlag, type TraceResult, checkText, createInit, lint, listDictionaries, parseApplicationFeatureFlags, suggestions, trace };
@@ -1,2 +1,2 @@
1
- import { a as parseApplicationFeatureFlags, c as listDictionaries, i as lint, n as checkText, o as suggestions, r as createInit, s as trace, t as IncludeExcludeFlag } from "./application-CdcDwTFc.js";
1
+ import { a as parseApplicationFeatureFlags, c as listDictionaries, i as lint, n as checkText, o as suggestions, r as createInit, s as trace, t as IncludeExcludeFlag } from "./application-DRa52DKq.js";
2
2
  export { IncludeExcludeFlag, checkText, createInit, lint, listDictionaries, parseApplicationFeatureFlags, suggestions, trace };