@konvert7/klint 0.38.0 → 1.0.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.
- package/README.md +49 -166
- package/bin/klint.js +47 -0
- package/klint.schema.json +1 -1
- package/klint.schema.yaml +1 -1
- package/package.json +15 -25
- package/cli/command.ts +0 -189
- package/cli/commands.ts +0 -90
- package/cli/help.ts +0 -21
- package/cli/install-skill.ts +0 -260
- package/cli/output.ts +0 -178
- package/cli/paths.ts +0 -20
- package/cli/rust-config.ts +0 -145
- package/cli/rust-engine.ts +0 -323
- package/cli/stats.ts +0 -92
- package/cli.ts +0 -214
- package/core/arch-comments.ts +0 -125
- package/core/arch.ts +0 -470
- package/core/ast.ts +0 -63
- package/core/config.schema.ts +0 -413
- package/core/fixer.ts +0 -47
- package/core/native-binary.ts +0 -123
- package/core/paths.ts +0 -9
- package/core/rule-helpers.ts +0 -76
- package/core/runner.ts +0 -196
- package/core/schema-version.ts +0 -46
- package/core/skill.ts +0 -164
- package/core/stats.ts +0 -160
- package/core/types.ts +0 -146
- package/plugins/index.ts +0 -6
- package/plugins/sonar.ts +0 -27
- package/rules/index.ts +0 -26
- package/rules/no-async-predicate.ts +0 -51
- package/rules/no-consecutive-array-push.ts +0 -61
- package/rules/no-date-equality.ts +0 -35
- package/rules/no-floating-promise.ts +0 -37
- package/rules/no-misused-promises.ts +0 -59
- package/rules/no-nested-template-literals.ts +0 -47
- package/rules/no-object-in-template.ts +0 -95
- package/rules/no-optional-chain-on-non-nullable.ts +0 -47
- package/rules/no-single-char-class.ts +0 -113
- package/rules/no-string-match.ts +0 -63
- package/rules/no-sync-in-async.ts +0 -43
- package/rules/no-unguarded-json-parse.ts +0 -35
- package/rules/prefer-at.ts +0 -46
- package/rules/prefer-nullish-coalescing-assign.ts +0 -111
- package/rules/prefer-string-raw-regexp.ts +0 -93
- package/rules/prefer-string-raw.ts +0 -39
- package/rules/prefer-string-replaceall.ts +0 -67
- package/skill/klint-rules/SKILL.md +0 -264
- package/tools/generate-homebrew-formula.ts +0 -64
- package/tools/prepare-native-release.ts +0 -166
- package/tools/prepare-nuget-release.ts +0 -62
- package/tools/prepare-python-release.ts +0 -76
- package/tools/semantic-release-crates.js +0 -96
- package/tools/semantic-release-native-npm.js +0 -121
- package/tools/semantic-release-root-npm.js +0 -87
package/cli/stats.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { type KlintStats, readStats, STATS_DIR, STATS_FILE } from "../core/stats";
|
|
3
|
-
import type { OptionValues } from "./command";
|
|
4
|
-
|
|
5
|
-
const RULE_HEADING = "by rule";
|
|
6
|
-
const REPORTED_HEADING = "reported";
|
|
7
|
-
const PREVENTED_HEADING = "prevented";
|
|
8
|
-
const LABEL_WIDTH = "violations reported".length;
|
|
9
|
-
|
|
10
|
-
interface RuleRow {
|
|
11
|
-
rule: string;
|
|
12
|
-
reported: number;
|
|
13
|
-
prevented: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function ruleRows(stats: KlintStats): RuleRow[] {
|
|
17
|
-
const rules = new Set([
|
|
18
|
-
...Object.keys(stats.reported.byRule),
|
|
19
|
-
...Object.keys(stats.prevented.byRule),
|
|
20
|
-
]);
|
|
21
|
-
const byCodePoint = (a: string, b: string) => Number(a > b) - Number(a < b);
|
|
22
|
-
return [...rules]
|
|
23
|
-
.map((rule) => ({
|
|
24
|
-
rule,
|
|
25
|
-
reported: stats.reported.byRule[rule] ?? 0,
|
|
26
|
-
prevented: stats.prevented.byRule[rule] ?? 0,
|
|
27
|
-
}))
|
|
28
|
-
.sort(
|
|
29
|
-
(a, b) =>
|
|
30
|
-
b.reported - a.reported ||
|
|
31
|
-
b.prevented - a.prevented ||
|
|
32
|
-
byCodePoint(a.rule, b.rule)
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface Summary {
|
|
37
|
-
label: string;
|
|
38
|
-
total: number;
|
|
39
|
-
detail: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function summaryLines(summaries: Summary[]): string[] {
|
|
43
|
-
const width = Math.max(...summaries.map(({ total }) => String(total).length));
|
|
44
|
-
return summaries.map(
|
|
45
|
-
({ label, total, detail }) =>
|
|
46
|
-
` ${label.padEnd(LABEL_WIDTH)} ${String(total).padStart(width)} (${detail})`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function ruleTable(rows: RuleRow[]): string[] {
|
|
51
|
-
const width = Math.max(RULE_HEADING.length - 2, ...rows.map(({ rule }) => rule.length));
|
|
52
|
-
const heading = ` ${RULE_HEADING.padEnd(width + 2)} ${REPORTED_HEADING} ${PREVENTED_HEADING}`;
|
|
53
|
-
const lines = rows.map(
|
|
54
|
-
({ rule, reported, prevented }) =>
|
|
55
|
-
` ${rule.padEnd(width)} ${String(reported).padStart(REPORTED_HEADING.length)} ${String(prevented).padStart(PREVENTED_HEADING.length)}`
|
|
56
|
-
);
|
|
57
|
-
return [heading, ...lines];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function formatStats(stats: KlintStats): string {
|
|
61
|
-
const { runs, reported, prevented } = stats;
|
|
62
|
-
const lines = [
|
|
63
|
-
`klint stats (${STATS_DIR}/${STATS_FILE})`,
|
|
64
|
-
"",
|
|
65
|
-
...summaryLines([
|
|
66
|
-
{
|
|
67
|
-
label: "runs",
|
|
68
|
-
total: runs.hook + runs.direct,
|
|
69
|
-
detail: `hook ${runs.hook}, direct ${runs.direct}`,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
label: "violations reported",
|
|
73
|
-
total: reported.bySource.hook + reported.bySource.direct,
|
|
74
|
-
detail: `hook ${reported.bySource.hook}, direct ${reported.bySource.direct}`,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
label: "errors prevented",
|
|
78
|
-
total: prevented.bySource.hook + prevented.bySource.direct,
|
|
79
|
-
detail: `caught by hook ${prevented.bySource.hook}, direct ${prevented.bySource.direct}`,
|
|
80
|
-
},
|
|
81
|
-
]),
|
|
82
|
-
];
|
|
83
|
-
const rows = ruleRows(stats);
|
|
84
|
-
if (rows.length > 0) lines.push("", ...ruleTable(rows));
|
|
85
|
-
return `${lines.join("\n")}\n`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function printStats(values: OptionValues): void {
|
|
89
|
-
const configDir =
|
|
90
|
-
typeof values.config === "string" ? resolve(values.config) : process.cwd();
|
|
91
|
-
process.stdout.write(formatStats(readStats(configDir)));
|
|
92
|
-
}
|
package/cli.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import { parse as parseYaml } from "yaml";
|
|
6
|
-
import { type OptionValues, runCommand } from "./cli/command";
|
|
7
|
-
import { klintCommand } from "./cli/commands";
|
|
8
|
-
import { installSkill } from "./cli/install-skill";
|
|
9
|
-
import {
|
|
10
|
-
formatDuration,
|
|
11
|
-
toJsonCliResult,
|
|
12
|
-
withAdvisories,
|
|
13
|
-
writeDebugEvent,
|
|
14
|
-
writeTextOutput,
|
|
15
|
-
} from "./cli/output";
|
|
16
|
-
import { installedVersion, SHIPPED_SKILL_PATH } from "./cli/paths";
|
|
17
|
-
import { runAutoEngine, runCompareEngine, runRustEngine } from "./cli/rust-engine";
|
|
18
|
-
import { printStats } from "./cli/stats";
|
|
19
|
-
import { applyFixes } from "./core/fixer";
|
|
20
|
-
import { runKlint } from "./core/runner";
|
|
21
|
-
import { schemaVersionAdvisory } from "./core/schema-version";
|
|
22
|
-
import { skillAdvisories } from "./core/skill";
|
|
23
|
-
import { runRecorder } from "./core/stats";
|
|
24
|
-
import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core/types";
|
|
25
|
-
|
|
26
|
-
export async function main(): Promise<number> {
|
|
27
|
-
const startedAt = performance.now();
|
|
28
|
-
const command = klintCommand({
|
|
29
|
-
lint: (values) => lint(values, startedAt),
|
|
30
|
-
stats: printStats,
|
|
31
|
-
installSkill,
|
|
32
|
-
});
|
|
33
|
-
return runCommand(command, process.argv.slice(2), ["klint"]);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function optionalPath(value: string | true | undefined): string | undefined {
|
|
37
|
-
return typeof value === "string" ? resolve(value) : undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function lint(values: OptionValues, startedAt: number): Promise<void> {
|
|
41
|
-
if (values.version) {
|
|
42
|
-
process.stdout.write(`klint ${installedVersion()}\n`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const rulesFile = optionalPath(values.rules);
|
|
46
|
-
const fix = values.fix === true;
|
|
47
|
-
const json = values.json === true;
|
|
48
|
-
const debug = values.debug === true;
|
|
49
|
-
const hook = values.hook === true;
|
|
50
|
-
const engine =
|
|
51
|
-
typeof values.engine === "string" ? values.engine : process.env.KLINT_ENGINE;
|
|
52
|
-
const configDir = optionalPath(values.config) ?? process.cwd();
|
|
53
|
-
const record = runRecorder(configDir, hook ? "hook" : "direct");
|
|
54
|
-
|
|
55
|
-
const yamlPath = resolve(configDir, "klint.yaml");
|
|
56
|
-
const jsonPath = resolve(configDir, "klint.config.json");
|
|
57
|
-
const usingYaml = existsSync(yamlPath);
|
|
58
|
-
const configPath = usingYaml ? yamlPath : jsonPath;
|
|
59
|
-
|
|
60
|
-
if (!existsSync(configPath)) {
|
|
61
|
-
process.stderr.write(
|
|
62
|
-
`klint: no config file found — create klint.yaml (or klint.config.json) at ${configDir}\n`
|
|
63
|
-
);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface RawConfig {
|
|
68
|
-
$schema?: string;
|
|
69
|
-
root?: string;
|
|
70
|
-
include?: string[];
|
|
71
|
-
plugins?: string[];
|
|
72
|
-
rules?: Record<string, RuleConfigValue>;
|
|
73
|
-
arch?: unknown;
|
|
74
|
-
}
|
|
75
|
-
let raw: RawConfig;
|
|
76
|
-
let configText = "";
|
|
77
|
-
try {
|
|
78
|
-
configText = await readFile(configPath, "utf-8");
|
|
79
|
-
raw = (usingYaml ? parseYaml(configText) : JSON.parse(configText)) as RawConfig;
|
|
80
|
-
} catch {
|
|
81
|
-
process.stderr.write(`klint: failed to parse ${configPath}\n`);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
const root = resolve(configDir, raw.root ?? ".");
|
|
85
|
-
|
|
86
|
-
const installed = installedVersion();
|
|
87
|
-
const advisories = [
|
|
88
|
-
...schemaVersionAdvisory({
|
|
89
|
-
schema: raw.$schema,
|
|
90
|
-
installed,
|
|
91
|
-
configFile: usingYaml ? "klint.yaml" : "klint.config.json",
|
|
92
|
-
configText,
|
|
93
|
-
}),
|
|
94
|
-
...skillAdvisories({
|
|
95
|
-
configDir,
|
|
96
|
-
installed,
|
|
97
|
-
shippedSkillPath: SHIPPED_SKILL_PATH,
|
|
98
|
-
}),
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
if (engine === "rust") {
|
|
102
|
-
runRustEngine({ advisories, fix, json, raw, record, root, rulesFile, startedAt });
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (
|
|
106
|
-
engine !== undefined &&
|
|
107
|
-
engine !== "ts" &&
|
|
108
|
-
engine !== "rust" &&
|
|
109
|
-
engine !== "compare" &&
|
|
110
|
-
engine !== "auto"
|
|
111
|
-
) {
|
|
112
|
-
process.stderr.write(
|
|
113
|
-
`klint: unknown engine "${engine}" (expected "ts", "rust", "compare", or "auto")\n`
|
|
114
|
-
);
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let customRules: Record<string, KlintRule> = {};
|
|
119
|
-
const defaultRulesPath = resolve(configDir, "klint.rules.ts");
|
|
120
|
-
const rulesPath =
|
|
121
|
-
rulesFile ?? (existsSync(defaultRulesPath) ? defaultRulesPath : undefined);
|
|
122
|
-
if (rulesPath) {
|
|
123
|
-
const mod = await import(rulesPath);
|
|
124
|
-
customRules = (mod.default ?? {}) as Record<string, KlintRule>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const customRulesMap: Record<string, RuleConfigValue> = Object.fromEntries(
|
|
128
|
-
Object.keys(customRules).map((name) => [name, "error" as const])
|
|
129
|
-
);
|
|
130
|
-
const allRules: KlintConfig["rules"] = { ...customRulesMap, ...(raw.rules ?? {}) };
|
|
131
|
-
|
|
132
|
-
if (engine === "auto") {
|
|
133
|
-
runAutoEngine({
|
|
134
|
-
advisories,
|
|
135
|
-
fix,
|
|
136
|
-
json,
|
|
137
|
-
raw,
|
|
138
|
-
record,
|
|
139
|
-
root,
|
|
140
|
-
rulesFile,
|
|
141
|
-
startedAt,
|
|
142
|
-
customRules,
|
|
143
|
-
customRulesMap,
|
|
144
|
-
});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const violations = runKlint(
|
|
149
|
-
{
|
|
150
|
-
root,
|
|
151
|
-
include: raw.include ?? ["."],
|
|
152
|
-
plugins: raw.plugins,
|
|
153
|
-
rules: allRules,
|
|
154
|
-
arch: raw.arch as ArchConfig | undefined,
|
|
155
|
-
},
|
|
156
|
-
customRules,
|
|
157
|
-
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
if (engine === "compare") {
|
|
161
|
-
runCompareEngine({
|
|
162
|
-
advisories,
|
|
163
|
-
fix,
|
|
164
|
-
json,
|
|
165
|
-
raw,
|
|
166
|
-
record,
|
|
167
|
-
root,
|
|
168
|
-
rulesFile,
|
|
169
|
-
tsViolations: violations,
|
|
170
|
-
});
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (json) {
|
|
175
|
-
record(violations);
|
|
176
|
-
const tsResult = toJsonCliResult(violations);
|
|
177
|
-
process.stdout.write(withAdvisories(tsResult.stdout, advisories));
|
|
178
|
-
process.exit(tsResult.status);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (fix) {
|
|
182
|
-
let totalApplied = 0;
|
|
183
|
-
let current = violations;
|
|
184
|
-
while (true) {
|
|
185
|
-
const applied = applyFixes(current, root);
|
|
186
|
-
totalApplied += applied;
|
|
187
|
-
if (applied === 0) break;
|
|
188
|
-
current = runKlint(
|
|
189
|
-
{
|
|
190
|
-
root,
|
|
191
|
-
include: raw.include ?? ["."],
|
|
192
|
-
plugins: raw.plugins,
|
|
193
|
-
rules: allRules,
|
|
194
|
-
arch: raw.arch as ArchConfig | undefined,
|
|
195
|
-
},
|
|
196
|
-
customRules,
|
|
197
|
-
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
198
|
-
);
|
|
199
|
-
if (current.every((v) => !v.fix)) break;
|
|
200
|
-
}
|
|
201
|
-
record(current);
|
|
202
|
-
const unfixed = current.filter((v) => !v.fix).length;
|
|
203
|
-
const msg =
|
|
204
|
-
unfixed > 0
|
|
205
|
-
? `klint: applied ${totalApplied} fix(es). ${unfixed} violation(s) require manual attention.`
|
|
206
|
-
: `klint: applied ${totalApplied} fix(es). No remaining violations.`;
|
|
207
|
-
process.stdout.write(`${msg} Finished in ${formatDuration(startedAt)}.\n`);
|
|
208
|
-
process.exit(0);
|
|
209
|
-
}
|
|
210
|
-
record(violations);
|
|
211
|
-
writeTextOutput([...violations, ...advisories], startedAt);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (import.meta.main) process.exitCode = await main();
|
package/core/arch-comments.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
|
|
3
|
-
interface CommentSpan {
|
|
4
|
-
startLine: number;
|
|
5
|
-
endLine: number;
|
|
6
|
-
isDoc: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** JS-like sources are lexed for comments; Python/Swift comment rules run in the native engine. */
|
|
10
|
-
function collectComments(file: string, content: string): CommentSpan[] {
|
|
11
|
-
if (!/\.(tsx?|jsx?|mts|cts)$/.test(file)) return [];
|
|
12
|
-
const variant = /\.(tsx|jsx)$/.test(file)
|
|
13
|
-
? ts.LanguageVariant.JSX
|
|
14
|
-
: ts.LanguageVariant.Standard;
|
|
15
|
-
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, variant, content);
|
|
16
|
-
const starts = lineStarts(content);
|
|
17
|
-
const spans: CommentSpan[] = [];
|
|
18
|
-
const substitutionDepths: number[] = [];
|
|
19
|
-
let braceDepth = 0;
|
|
20
|
-
let token = scanner.scan();
|
|
21
|
-
while (token !== ts.SyntaxKind.EndOfFileToken) {
|
|
22
|
-
if (
|
|
23
|
-
token === ts.SyntaxKind.SingleLineCommentTrivia ||
|
|
24
|
-
token === ts.SyntaxKind.MultiLineCommentTrivia
|
|
25
|
-
) {
|
|
26
|
-
const start = scanner.getTokenStart();
|
|
27
|
-
const end = scanner.getTokenEnd();
|
|
28
|
-
spans.push({
|
|
29
|
-
startLine: lineAt(starts, start),
|
|
30
|
-
endLine: lineAt(starts, Math.max(start, end - 1)),
|
|
31
|
-
isDoc: isDocComment(content.slice(start, end)),
|
|
32
|
-
});
|
|
33
|
-
} else if (token === ts.SyntaxKind.TemplateHead) {
|
|
34
|
-
substitutionDepths.push(braceDepth);
|
|
35
|
-
} else if (token === ts.SyntaxKind.OpenBraceToken) {
|
|
36
|
-
braceDepth += 1;
|
|
37
|
-
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
|
38
|
-
if (substitutionDepths.at(-1) === braceDepth) {
|
|
39
|
-
if (scanner.reScanTemplateToken(false) === ts.SyntaxKind.TemplateTail) {
|
|
40
|
-
substitutionDepths.pop();
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
braceDepth -= 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
token = scanner.scan();
|
|
47
|
-
}
|
|
48
|
-
return spans;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** A `/** */` JSDoc block, but not the empty `/**/` comment. Mirrors the Rust engine. */
|
|
52
|
-
function isDocComment(text: string): boolean {
|
|
53
|
-
return text.startsWith("/**") && text !== "/**/";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function commentLineSet(
|
|
57
|
-
file: string,
|
|
58
|
-
content: string,
|
|
59
|
-
countDocComments: boolean
|
|
60
|
-
): Set<number> {
|
|
61
|
-
const set = new Set<number>();
|
|
62
|
-
for (const span of collectComments(file, content)) {
|
|
63
|
-
if (!countDocComments && span.isDoc) continue;
|
|
64
|
-
for (let line = span.startLine; line <= span.endLine; line++) set.add(line);
|
|
65
|
-
}
|
|
66
|
-
return set;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Comment lines whose source text matches an `ignore` pattern. They stay in the
|
|
71
|
-
* density denominator and keep a comment run contiguous, but count toward neither.
|
|
72
|
-
*/
|
|
73
|
-
export function ignoredCommentLines(
|
|
74
|
-
content: string,
|
|
75
|
-
commentLines: Iterable<number>,
|
|
76
|
-
matchesIgnore: (text: string) => boolean
|
|
77
|
-
): Set<number> {
|
|
78
|
-
const sourceLines = content.split("\n");
|
|
79
|
-
const ignored = new Set<number>();
|
|
80
|
-
for (const line of commentLines) {
|
|
81
|
-
if (matchesIgnore(sourceLines[line - 1] ?? "")) ignored.add(line);
|
|
82
|
-
}
|
|
83
|
-
return ignored;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Returns the first line at which a run of consecutive comment lines exceeds
|
|
88
|
-
* `limit`. An ignored line neither counts toward the height nor breaks the run.
|
|
89
|
-
*/
|
|
90
|
-
export function firstCommentBlockOverrun(
|
|
91
|
-
sortedLines: number[],
|
|
92
|
-
limit: number,
|
|
93
|
-
ignored?: ReadonlySet<number>
|
|
94
|
-
): number | undefined {
|
|
95
|
-
let counted = 0;
|
|
96
|
-
let prev: number | undefined;
|
|
97
|
-
for (const line of sortedLines) {
|
|
98
|
-
if (prev !== undefined && line !== prev + 1) counted = 0;
|
|
99
|
-
prev = line;
|
|
100
|
-
if (ignored?.has(line)) continue;
|
|
101
|
-
counted += 1;
|
|
102
|
-
if (counted > limit) return line;
|
|
103
|
-
}
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function lineStarts(content: string): number[] {
|
|
108
|
-
const starts = [0];
|
|
109
|
-
for (let i = 0; i < content.length; i++) {
|
|
110
|
-
if (content[i] === "\n") starts.push(i + 1);
|
|
111
|
-
}
|
|
112
|
-
return starts;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** 1-based line for a character offset, via binary search over line starts. */
|
|
116
|
-
function lineAt(starts: number[], offset: number): number {
|
|
117
|
-
let lo = 0;
|
|
118
|
-
let hi = starts.length - 1;
|
|
119
|
-
while (lo < hi) {
|
|
120
|
-
const mid = (lo + hi + 1) >> 1;
|
|
121
|
-
if (starts[mid] <= offset) lo = mid;
|
|
122
|
-
else hi = mid - 1;
|
|
123
|
-
}
|
|
124
|
-
return lo + 1;
|
|
125
|
-
}
|