paperlint 11.1.0 → 11.1.2
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/.github/workflows/dependabot-automerge.yml +20 -6
- package/.github/workflows/release.yml +10 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +83 -7
- package/dist/cli.js.map +1 -1
- package/dist/submission-confirm-hook.d.ts +59 -0
- package/dist/submission-confirm-hook.d.ts.map +1 -0
- package/dist/submission-confirm-hook.js +230 -0
- package/dist/submission-confirm-hook.js.map +1 -0
- package/dist/submission.d.ts +13 -1
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +55 -7
- package/dist/submission.js.map +1 -1
- package/dist/venue-rules.d.ts +33 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +84 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/install.md +4 -4
- package/docs/layout.md +1 -0
- package/docs/submission.md +33 -1
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +4 -0
- package/scripts/refresh-deadlines/main.test.ts +5 -2
- package/scripts/refresh-deadlines/refresh.test.ts +15 -14
- package/skills/submission-portal/SKILL.md +11 -10
- package/skills/submission-portal/SKILL.md.spec.ts +10 -9
- package/src/cli.test.ts +56 -0
- package/src/cli.ts +100 -6
- package/src/cycle-rules.test.ts +14 -6
- package/src/hooks-settings.harness.ts +6 -6
- package/src/presets.test.ts +34 -24
- package/src/submission-confirm-hook.test.ts +399 -0
- package/src/submission-confirm-hook.ts +330 -0
- package/src/submission-fields-run.test.ts +1 -1
- package/src/submission-new.test.ts +29 -2
- package/src/submission-show.test.ts +9 -0
- package/src/submission-update.test.ts +153 -3
- package/src/submission.ts +85 -10
- package/src/venue-rules.test.ts +15 -0
- package/src/venue-rules.ts +126 -10
package/src/submission.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* sent (`src/domain/submission-fields.ts`). A checkbox field the portal did not take is said, and
|
|
19
19
|
* fails the command: HotCRP keeps an option it does not know as nothing, without an error.
|
|
20
20
|
*
|
|
21
|
+
* Before the portal is asked, `new` and `update` print what the venue rules find in the exact PDF
|
|
22
|
+
* they send, as `paperlint lint` prints it — or "venue rules: no errors" (paperlint#264). The verdict
|
|
23
|
+
* is the author's to act on; the one thing the command refuses is a `--save` whose build facts are
|
|
24
|
+
* missing or about other bytes, because then nothing was checked (exit 2).
|
|
25
|
+
*
|
|
21
26
|
* 🔴 THE TOKEN IS AN INPUT, AND ONLY AN INPUT. It comes from the environment (the root reads it), is
|
|
22
27
|
* handed to the adapter, and never appears in anything printed here.
|
|
23
28
|
*/
|
|
@@ -71,11 +76,12 @@ import {
|
|
|
71
76
|
} from "./domain/portal-token.ts";
|
|
72
77
|
import { LINE_BREAK, OPAQUE, runText } from "./domain/tex-document.ts";
|
|
73
78
|
import type { LatexReader } from "./ports/latex.ts";
|
|
74
|
-
import { factsPath, parseFactsText } from "./facts-file.ts";
|
|
79
|
+
import { factsPath, parseFactsText, type ReadFacts } from "./facts-file.ts";
|
|
75
80
|
import { lineColumn, readPaper } from "./tex-paper.ts";
|
|
76
81
|
import { originOf, type PaperSource } from "./domain/paper-source.ts";
|
|
77
82
|
import type { SourcesCodec } from "./ports/sources-codec.ts";
|
|
78
83
|
import { MAIN } from "./build.ts";
|
|
84
|
+
import { assessSent, venueMessages, type VenueMessage } from "./venue-rules.ts";
|
|
79
85
|
|
|
80
86
|
/** How to make and set the site's token: the page, the scopes, the variable. Pure. */
|
|
81
87
|
export function tokenSteps(
|
|
@@ -157,12 +163,21 @@ export interface SubmissionDeps {
|
|
|
157
163
|
readonly latex: LatexReader;
|
|
158
164
|
/** Reads the build's record of the files TeX read, for the includes (`new`). */
|
|
159
165
|
readonly codec: SourcesCodec;
|
|
166
|
+
/** Findings on one file, printed as `paperlint lint` prints them (the root passes ESLint's `stylish`). */
|
|
167
|
+
readonly lintText: (
|
|
168
|
+
file: string,
|
|
169
|
+
messages: readonly VenueMessage[],
|
|
170
|
+
) => Promise<string>;
|
|
160
171
|
readonly log: (line: string) => void;
|
|
161
172
|
readonly err: (line: string) => void;
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
/** A paper's portal, its submission there, and the PDF the paper builds. */
|
|
165
176
|
export interface SubmissionTarget {
|
|
177
|
+
/** The paper's folder, absolute. */
|
|
178
|
+
readonly paperDir: string;
|
|
179
|
+
/** The paper's settings and venue preset, resolved: what the venue rules judge the PDF against. */
|
|
180
|
+
readonly resolved: Extract<PaperPreset, { readonly kind: "resolved" }>;
|
|
166
181
|
readonly portal: SupportedPortal;
|
|
167
182
|
/** The paper's submission on the portal; null before `new` created one. */
|
|
168
183
|
readonly id: number | null;
|
|
@@ -194,6 +209,12 @@ export interface SubmissionTarget {
|
|
|
194
209
|
|
|
195
210
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
196
211
|
|
|
212
|
+
/** The PDF a paper sends when no `--pdf` is named: its `pdf` setting, else `paper.pdf`. Pure. */
|
|
213
|
+
export const paperPdf = (
|
|
214
|
+
paperDir: string,
|
|
215
|
+
settings: Pick<PaperSettings, "pdf"> | null,
|
|
216
|
+
): string => join(paperDir, settings?.pdf ?? "paper.pdf");
|
|
217
|
+
|
|
197
218
|
/** Why a paper without a venue preset has no portal: the key to set, or — with cycles — where. */
|
|
198
219
|
const noPresetLine = (
|
|
199
220
|
paperDir: string,
|
|
@@ -241,6 +262,8 @@ function targetOf(
|
|
|
241
262
|
? []
|
|
242
263
|
: currentDeadlines(p.settings.cycles.current);
|
|
243
264
|
return ok({
|
|
265
|
+
paperDir,
|
|
266
|
+
resolved: p,
|
|
244
267
|
portal: portal.value,
|
|
245
268
|
id: p.settings.submission?.id ?? null,
|
|
246
269
|
config: configOf(paperDir),
|
|
@@ -249,7 +272,7 @@ function targetOf(
|
|
|
249
272
|
fields: {},
|
|
250
273
|
venueFields: p.preset.submissionFields,
|
|
251
274
|
accepted: p.settings.cycles?.current.kind === "accepted",
|
|
252
|
-
pdf:
|
|
275
|
+
pdf: paperPdf(paperDir, p.settings),
|
|
253
276
|
call: identity.type === "venue" ? identity.url : null,
|
|
254
277
|
deadlines: deadlinesInForce(own, p.preset.deadlines),
|
|
255
278
|
});
|
|
@@ -477,6 +500,53 @@ const changeOf = (
|
|
|
477
500
|
fields,
|
|
478
501
|
});
|
|
479
502
|
|
|
503
|
+
/**
|
|
504
|
+
* The venue rules on the bytes about to be sent, printed as `paperlint lint` prints them (ESLint's
|
|
505
|
+
* `stylish`), or one line when they find nothing. Returns whether a `--save` stops, said: the
|
|
506
|
+
* build's facts are missing, unreadable or about other bytes, so the rules did not see this PDF.
|
|
507
|
+
*/
|
|
508
|
+
async function venueCheck(
|
|
509
|
+
t: SubmissionTarget,
|
|
510
|
+
bytes: Uint8Array,
|
|
511
|
+
save: boolean,
|
|
512
|
+
deps: RunDeps,
|
|
513
|
+
): Promise<boolean> {
|
|
514
|
+
const a = assessSent(t.paperDir, t.resolved, bytes, deps.presets);
|
|
515
|
+
const tex = join(t.paperDir, MAIN);
|
|
516
|
+
const source = bytesAt(deps.files, tex);
|
|
517
|
+
const messages = venueMessages(
|
|
518
|
+
a,
|
|
519
|
+
source === null ? null : new TextDecoder().decode(source),
|
|
520
|
+
);
|
|
521
|
+
const printed = (await deps.lintText(tex, messages)).trim();
|
|
522
|
+
logAll(
|
|
523
|
+
printed === ""
|
|
524
|
+
? [nothingFound(a.kind)]
|
|
525
|
+
: [`${VENUE_RULES}:`, ...printed.split("\n")],
|
|
526
|
+
deps,
|
|
527
|
+
);
|
|
528
|
+
// Checked on its own: a cycle still porting skips the venue rules, not this.
|
|
529
|
+
const refused = save && factsOfSent(t.paperDir, bytes, deps) === null;
|
|
530
|
+
if (refused) deps.err(REBUILD_REFUSAL);
|
|
531
|
+
return refused;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** The line for a check that found nothing — which, while a cycle is porting, is that nothing ran. */
|
|
535
|
+
const nothingFound = (kind: string): string =>
|
|
536
|
+
kind === "porting"
|
|
537
|
+
? "venue rules: none run — the current cycle's phase is \"porting\", so the venue's format does not apply yet"
|
|
538
|
+
: `${VENUE_RULES}: no errors`;
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* What the check is, said where it prints: the venue rules at paperlint's default levels and
|
|
542
|
+
* options. A paper's or the project's `rules` overrides are not read here; `paperlint lint` reads them.
|
|
543
|
+
*/
|
|
544
|
+
const VENUE_RULES = "venue rules at their default options";
|
|
545
|
+
|
|
546
|
+
/** What `--save` says when the build's facts do not describe the PDF: nothing was sent. */
|
|
547
|
+
const REBUILD_REFUSAL =
|
|
548
|
+
"refused: no build facts describe this PDF (none, unreadable, or of other bytes), so the venue rules did not see it — rebuild (`paperlint build`), run the dry run again, and read its venue check; nothing was sent";
|
|
549
|
+
|
|
480
550
|
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
481
551
|
async function runUpdate(
|
|
482
552
|
t: SubmissionTarget & { readonly id: number },
|
|
@@ -493,7 +563,10 @@ async function runUpdate(
|
|
|
493
563
|
);
|
|
494
564
|
const abstract = abstractOf(a, deps);
|
|
495
565
|
if (!abstract.ok) return (deps.err(abstract.error), 2);
|
|
496
|
-
|
|
566
|
+
deps.log(checkPdfLine(shown, t.call));
|
|
567
|
+
const refused = await venueCheck(t, bytes, a.save, deps);
|
|
568
|
+
logAll(fieldLines(t.fields), deps);
|
|
569
|
+
if (refused) return 2;
|
|
497
570
|
const change = changeOf(a, bytes, abstract.value, t.fields);
|
|
498
571
|
const r = await portal.update(t.id, change, { save: a.save });
|
|
499
572
|
if (!r.ok) return failed(r.error, deps);
|
|
@@ -508,19 +581,19 @@ async function runUpdate(
|
|
|
508
581
|
// ── new ──────────────────────────────────────────────────────────────────────────────
|
|
509
582
|
|
|
510
583
|
/**
|
|
511
|
-
* The
|
|
512
|
-
* there are
|
|
584
|
+
* The build's facts about the PDF that is sent — what the title falls back on, and what a `--save`
|
|
585
|
+
* needs to exist; null when there are none, they do not read, or they describe other bytes.
|
|
513
586
|
*/
|
|
514
|
-
function
|
|
587
|
+
function factsOfSent(
|
|
515
588
|
paperDir: string,
|
|
516
589
|
pdf: Uint8Array,
|
|
517
|
-
deps: SubmissionDeps,
|
|
518
|
-
):
|
|
590
|
+
deps: Pick<SubmissionDeps, "files">,
|
|
591
|
+
): ReadFacts | null {
|
|
519
592
|
const bytes = bytesAt(deps.files, factsPath(paperDir));
|
|
520
593
|
const facts =
|
|
521
594
|
bytes === null ? null : parseFactsText(new TextDecoder().decode(bytes));
|
|
522
595
|
return facts?.ok === true && facts.value.sha === sha256Hex(pdf)
|
|
523
|
-
? facts.value
|
|
596
|
+
? facts.value
|
|
524
597
|
: null;
|
|
525
598
|
}
|
|
526
599
|
|
|
@@ -734,7 +807,7 @@ function titleAndAbstract(
|
|
|
734
807
|
const own = abstractOf(a, deps);
|
|
735
808
|
if (!own.ok) return own;
|
|
736
809
|
const paper = assembledPaper(a.paperDir, deps);
|
|
737
|
-
const metadata =
|
|
810
|
+
const metadata = factsOfSent(a.paperDir, pdf, deps)?.text.metadata ?? null;
|
|
738
811
|
const title = titleOf(
|
|
739
812
|
paper === null
|
|
740
813
|
? null
|
|
@@ -824,7 +897,9 @@ async function runNew(
|
|
|
824
897
|
if (!n.ok) return (deps.err(n.error), 2);
|
|
825
898
|
const { paper, shown } = n.value;
|
|
826
899
|
deps.log(checkPdfLine(shown, t.call));
|
|
900
|
+
const refused = await venueCheck(t, paper.pdf.bytes, a.save, deps);
|
|
827
901
|
logAll(newLines(paper), deps);
|
|
902
|
+
if (refused) return 2;
|
|
828
903
|
const r = await portal.create(paper, { save: a.save });
|
|
829
904
|
if (!r.ok) return failed(r.error, deps);
|
|
830
905
|
const dropped = droppedLines(t.fields, t.venueFields, r.value);
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { presetsDir } from "./package-dirs.ts";
|
|
|
16
16
|
import {
|
|
17
17
|
ANONYMITY_RULE_LEVELS,
|
|
18
18
|
anonymityRules,
|
|
19
|
+
findingText,
|
|
19
20
|
judgeBodyEnd,
|
|
20
21
|
PAGE_LIMIT_RULE_LEVELS,
|
|
21
22
|
pageLimitRules,
|
|
@@ -1275,3 +1276,17 @@ describe("the cycle decides what applies", () => {
|
|
|
1275
1276
|
expect(lint({ venue: onCycle({ outcome: accepted }), facts })).toEqual([]);
|
|
1276
1277
|
});
|
|
1277
1278
|
});
|
|
1279
|
+
|
|
1280
|
+
describe("findingText — a finding outside ESLint, as ESLint prints it", () => {
|
|
1281
|
+
it("fills each placeholder from the data, leaves one the data lacks as written, and prints an unknown id as itself", () => {
|
|
1282
|
+
expect(
|
|
1283
|
+
findingText("page-limit", {
|
|
1284
|
+
messageId: "pages",
|
|
1285
|
+
data: { what: "body pages", got: 3, max: 2, venue: "v" },
|
|
1286
|
+
}),
|
|
1287
|
+
).toBe(
|
|
1288
|
+
"body pages: 3, over the limit 2 for v/{{kind}} — a desk reject; cut the text",
|
|
1289
|
+
);
|
|
1290
|
+
expect(findingText("fonts", { messageId: "nope", data: {} })).toBe("nope");
|
|
1291
|
+
});
|
|
1292
|
+
});
|
package/src/venue-rules.ts
CHANGED
|
@@ -51,7 +51,13 @@ import {
|
|
|
51
51
|
type ReadFacts,
|
|
52
52
|
} from "./facts-file.ts";
|
|
53
53
|
import type { KindLimits, VenueFormat } from "./tex-requirements.ts";
|
|
54
|
-
import {
|
|
54
|
+
import { entries } from "remeda";
|
|
55
|
+
import {
|
|
56
|
+
paperPreset,
|
|
57
|
+
presetProblemText,
|
|
58
|
+
type PaperPreset,
|
|
59
|
+
type Preset,
|
|
60
|
+
} from "./presets.ts";
|
|
55
61
|
import type { PaperSettings } from "./paper-settings.ts";
|
|
56
62
|
import type { FlatGeometry } from "./domain/geometry.ts";
|
|
57
63
|
import { callerPath } from "./caller-path.ts";
|
|
@@ -171,18 +177,25 @@ const reviewOf = (
|
|
|
171
177
|
identity: settings.identity,
|
|
172
178
|
});
|
|
173
179
|
|
|
180
|
+
const isFinding = (v: object): v is Finding => "messageId" in v;
|
|
181
|
+
|
|
182
|
+
/** The facts, read, or the `pdf/fresh` finding that says why they cannot be. */
|
|
183
|
+
function readFacts(body: string): ReadFacts | Finding {
|
|
184
|
+
const parsed = parseFactsText(body);
|
|
185
|
+
if (parsed.ok) return parsed.value;
|
|
186
|
+
return parsed.error.kind === "schema"
|
|
187
|
+
? finding("schema", { got: parsed.error.got, want: FACTS_SCHEMA })
|
|
188
|
+
: finding("factsBroken", { why: parsed.error.why });
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
/** The facts about the PDF on disk, or the `pdf/fresh` finding that says why they are not. */
|
|
175
192
|
function freshFacts(
|
|
176
193
|
files: Files,
|
|
177
194
|
paperDir: string,
|
|
178
195
|
body: string,
|
|
179
196
|
): ReadFacts | Finding {
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
return parsed.error.kind === "schema"
|
|
183
|
-
? finding("schema", { got: parsed.error.got, want: FACTS_SCHEMA })
|
|
184
|
-
: finding("factsBroken", { why: parsed.error.why });
|
|
185
|
-
const f = parsed.value;
|
|
197
|
+
const f = readFacts(body);
|
|
198
|
+
if (isFinding(f)) return f;
|
|
186
199
|
const pdf = files.readBytes(
|
|
187
200
|
callerPath(isAbsolute(f.pdf) ? f.pdf : join(paperDir, f.pdf)),
|
|
188
201
|
);
|
|
@@ -190,8 +203,6 @@ function freshFacts(
|
|
|
190
203
|
return sha256Hex(pdf) === f.sha ? f : finding("stale", { pdf: f.pdf });
|
|
191
204
|
}
|
|
192
205
|
|
|
193
|
-
const isFinding = (v: object): v is Finding => "messageId" in v;
|
|
194
|
-
|
|
195
206
|
/** The paper's declared venue and kind, resolved: what the judges compare the facts against. */
|
|
196
207
|
const resolvedOf = (preset: Preset, settings: PaperSettings): Resolved => ({
|
|
197
208
|
venue: preset.label,
|
|
@@ -221,17 +232,49 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
|
|
|
221
232
|
kind: "unresolved",
|
|
222
233
|
finding: finding("preset", { why: presetProblemText(p.problem) }),
|
|
223
234
|
};
|
|
235
|
+
return assessBuild(paperDir, p, deps, (body) =>
|
|
236
|
+
freshFacts(deps.files, paperDir, body),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A paper whose preset resolved, against its build: `fresh` reads the facts and says whether they hold. */
|
|
241
|
+
function assessBuild(
|
|
242
|
+
paperDir: string,
|
|
243
|
+
p: Extract<PaperPreset, { readonly kind: "resolved" }>,
|
|
244
|
+
deps: VenueRuleDeps,
|
|
245
|
+
fresh: (factsText: string) => ReadFacts | Finding,
|
|
246
|
+
): Assessment {
|
|
224
247
|
if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
|
|
225
248
|
return { kind: "porting" };
|
|
226
249
|
const venue = resolvedOf(p.preset, p.settings);
|
|
227
250
|
const factsText = text(deps.files, factsPath(paperDir));
|
|
228
251
|
if (factsText === null) return { kind: "unbuilt", venue };
|
|
229
|
-
const facts =
|
|
252
|
+
const facts = fresh(factsText);
|
|
230
253
|
return isFinding(facts)
|
|
231
254
|
? { kind: "stale", finding: facts }
|
|
232
255
|
: { kind: "ready", venue, facts };
|
|
233
256
|
}
|
|
234
257
|
|
|
258
|
+
/**
|
|
259
|
+
* A paper whose preset resolved, assessed on `pdf` — the bytes `paperlint submission` is about to
|
|
260
|
+
* send. The facts hold when their SHA-256 is that of `pdf`; where the PDF they name is on disk, and
|
|
261
|
+
* whether it is still there, does not matter. Reads through `deps.files` only.
|
|
262
|
+
*/
|
|
263
|
+
export function assessSent(
|
|
264
|
+
paperDir: string,
|
|
265
|
+
p: Extract<PaperPreset, { readonly kind: "resolved" }>,
|
|
266
|
+
pdf: Uint8Array,
|
|
267
|
+
deps: VenueRuleDeps,
|
|
268
|
+
): Assessment {
|
|
269
|
+
const sha = sha256Hex(pdf);
|
|
270
|
+
return assessBuild(paperDir, p, deps, (body) => {
|
|
271
|
+
const f = readFacts(body);
|
|
272
|
+
return isFinding(f) || f.sha === sha
|
|
273
|
+
? f
|
|
274
|
+
: finding("notSent", { pdf: f.pdf });
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
235
278
|
// ── the judges: typed facts + typed format → findings. Pure. ─────────────────────────
|
|
236
279
|
|
|
237
280
|
/** Type 3, not embedded, and each family the venue names missing from every font. */
|
|
@@ -658,6 +701,8 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
658
701
|
"_build/paper.facts.json describes {{pdf}}, which is not on disk (a failed build removes it) — rebuild the paper",
|
|
659
702
|
stale:
|
|
660
703
|
"_build/paper.facts.json describes a DIFFERENT {{pdf}} than the one on disk (its SHA-256 differs), so the venue checks would judge an earlier build — rebuild the paper",
|
|
704
|
+
notSent:
|
|
705
|
+
"_build/paper.facts.json describes a {{pdf}} with another SHA-256 than the PDF being sent, so the venue checks would judge other bytes — rebuild the paper (`paperlint build`) from the source of the PDF you send",
|
|
661
706
|
},
|
|
662
707
|
},
|
|
663
708
|
profile: {
|
|
@@ -874,3 +919,74 @@ export function pageLimitRules(
|
|
|
874
919
|
): Readonly<Record<"page-limit", VenueRuleModule>> {
|
|
875
920
|
return { "page-limit": rule("page-limit", deps) };
|
|
876
921
|
}
|
|
922
|
+
|
|
923
|
+
// ── the same findings, outside ESLint ──────────────────────────────────────────────────
|
|
924
|
+
|
|
925
|
+
/** Each venue rule by the id lint reports it under. */
|
|
926
|
+
const LINT_IDS = {
|
|
927
|
+
fresh: "pdf/fresh",
|
|
928
|
+
profile: "pdf/profile",
|
|
929
|
+
measured: "pdf/measured",
|
|
930
|
+
"page-limit": "format/page-limit",
|
|
931
|
+
fonts: "pdf/fonts",
|
|
932
|
+
geometry: "pdf/geometry",
|
|
933
|
+
"body-size": "pdf/body-size",
|
|
934
|
+
anonymity: "anonymity/identity",
|
|
935
|
+
} as const satisfies Record<VenueRuleName, string>;
|
|
936
|
+
|
|
937
|
+
const LEVELS = {
|
|
938
|
+
...VENUE_RULE_LEVELS,
|
|
939
|
+
...PAGE_LIMIT_RULE_LEVELS,
|
|
940
|
+
...ANONYMITY_RULE_LEVELS,
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* A finding's message as ESLint prints it: the rule's message with `{{name}}` filled from its data,
|
|
945
|
+
* a placeholder the data lacks left as written, and an unknown message id as itself. Pure.
|
|
946
|
+
*/
|
|
947
|
+
export function findingText(rule: VenueRuleName, f: Finding): string {
|
|
948
|
+
const message = META[rule].messages[f.messageId] ?? f.messageId;
|
|
949
|
+
return message.replace(/\{\{([^{}]+?)\}\}/gu, (whole, name: string) => {
|
|
950
|
+
const value = f.data[name.trim()];
|
|
951
|
+
return value === undefined ? whole : String(value);
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** One finding as a lint message: the fields ESLint's formatters read from a `Linter.LintMessage`. */
|
|
956
|
+
export interface VenueMessage {
|
|
957
|
+
readonly ruleId: string;
|
|
958
|
+
readonly severity: 1 | 2;
|
|
959
|
+
readonly message: string;
|
|
960
|
+
readonly messageId: string;
|
|
961
|
+
readonly line: number;
|
|
962
|
+
readonly column: number;
|
|
963
|
+
readonly endLine: number;
|
|
964
|
+
readonly endColumn: number;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Every venue rule's findings on an assessed paper, as the lint messages `paperlint lint` would
|
|
969
|
+
* report on its `paper.tex` (`source`; line 1 without one): the same judges, at paperlint's default
|
|
970
|
+
* levels and options. `paperlint submission` prints them with lint's formatter before a send. Paths
|
|
971
|
+
* are shown as they are: the one message that shows a path (`pdf/measured` on a paper without a
|
|
972
|
+
* preset) is never reached once a preset resolved. Pure.
|
|
973
|
+
*/
|
|
974
|
+
export function venueMessages(
|
|
975
|
+
a: Assessment,
|
|
976
|
+
source: string | null,
|
|
977
|
+
): readonly VenueMessage[] {
|
|
978
|
+
const line = source === null ? 1 : reportLine(source);
|
|
979
|
+
return entries(LINT_IDS).flatMap(([name, ruleId]) =>
|
|
980
|
+
JUDGES[name](a, { options: [], shown: String }).map((f) => ({
|
|
981
|
+
ruleId,
|
|
982
|
+
severity: LEVELS[ruleId] === "error" ? 2 : 1,
|
|
983
|
+
message: findingText(name, f),
|
|
984
|
+
messageId: f.messageId,
|
|
985
|
+
// ESLint's columns count from 1: the rule reports column 1 counted from 0.
|
|
986
|
+
line,
|
|
987
|
+
column: 2,
|
|
988
|
+
endLine: line,
|
|
989
|
+
endColumn: 3,
|
|
990
|
+
})),
|
|
991
|
+
);
|
|
992
|
+
}
|