-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: split functionality internally (#117)
## PR Checklist - [x] Addresses an existing open issue: fixes #116 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/eslint-plugin-expect-type/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/eslint-plugin-expect-type/blob/main/.github/CONTRIBUTING.md) were taken ## Overview Splits out the roughly-not-specific-to-ESLint logic from the `expect` rule into: * `src/assertions`: parsing assertions out from source code * `src/failures`: determined unmatched/unmet assertions * `src/utils`: miscellaneous helpers
- Loading branch information
1 parent
6524dc1
commit 740cd78
Showing
9 changed files
with
662 additions
and
551 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import ts from "typescript"; | ||
|
||
import { getTypeSnapshot } from "../utils/snapshot.js"; | ||
import { parseTwoslashAssertion } from "./parseTwoslashAssertion.js"; | ||
import { Assertion, TwoSlashAssertion } from "./types.js"; | ||
import { Assertions, SyntaxError } from "./types.js"; | ||
|
||
export function parseAssertions(sourceFile: ts.SourceFile): Assertions { | ||
const errorLines = new Set<number>(); | ||
const typeAssertions = new Map<number, Assertion>(); | ||
const duplicates: number[] = []; | ||
const syntaxErrors: SyntaxError[] = []; | ||
const twoSlashAssertions: TwoSlashAssertion[] = []; | ||
|
||
const { text } = sourceFile; | ||
const commentRegexp = /\/\/(.*)/g; | ||
const lineStarts = sourceFile.getLineStarts(); | ||
let curLine = 0; | ||
|
||
while (true) { | ||
const commentMatch = commentRegexp.exec(text); | ||
if (commentMatch === null) { | ||
break; | ||
} | ||
|
||
// Match on the contents of that comment so we do nothing in a commented-out assertion, | ||
// i.e. `// foo; // $ExpectType number` | ||
const comment = commentMatch[1]; | ||
// eslint-disable-next-line regexp/no-unused-capturing-group | ||
const matchExpect = /^ ?\$Expect(TypeSnapshot|Type|Error)( (.*))?$/.exec( | ||
comment, | ||
) as [never, "Error" | "Type" | "TypeSnapshot", never, string?] | null; | ||
const commentIndex = commentMatch.index; | ||
const line = getLine(commentIndex); | ||
if (matchExpect) { | ||
const directive = matchExpect[1]; | ||
const payload = matchExpect[3]; | ||
switch (directive) { | ||
case "TypeSnapshot": | ||
const snapshotName = payload; | ||
if (snapshotName) { | ||
if (typeAssertions.delete(line)) { | ||
duplicates.push(line); | ||
} else { | ||
typeAssertions.set(line, { | ||
assertionType: "snapshot", | ||
expected: getTypeSnapshot(sourceFile.fileName, snapshotName), | ||
snapshotName, | ||
}); | ||
} | ||
} else { | ||
syntaxErrors.push({ | ||
line, | ||
type: "MissingSnapshotName", | ||
}); | ||
} | ||
|
||
break; | ||
|
||
case "Error": | ||
if (errorLines.has(line)) { | ||
duplicates.push(line); | ||
} | ||
|
||
errorLines.add(line); | ||
break; | ||
|
||
case "Type": { | ||
const expected = payload; | ||
if (expected) { | ||
// Don't bother with the assertion if there are 2 assertions on 1 line. Just fail for the duplicate. | ||
if (typeAssertions.delete(line)) { | ||
duplicates.push(line); | ||
} else { | ||
typeAssertions.set(line, { assertionType: "manual", expected }); | ||
} | ||
} else { | ||
syntaxErrors.push({ | ||
line, | ||
type: "MissingExpectType", | ||
}); | ||
} | ||
|
||
break; | ||
} | ||
} | ||
} else { | ||
// Maybe it's a twoslash assertion | ||
const assertion = parseTwoslashAssertion( | ||
comment, | ||
commentIndex, | ||
line, | ||
text, | ||
lineStarts, | ||
); | ||
if (assertion) { | ||
if ("type" in assertion) { | ||
syntaxErrors.push(assertion); | ||
} else { | ||
twoSlashAssertions.push(assertion); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return { | ||
duplicates, | ||
errorLines, | ||
syntaxErrors, | ||
twoSlashAssertions, | ||
typeAssertions, | ||
}; | ||
|
||
function getLine(pos: number): number { | ||
// advance curLine to be the line preceding 'pos' | ||
while (lineStarts[curLine + 1] <= pos) { | ||
curLine++; | ||
} | ||
|
||
// If this is the first token on the line, it applies to the next line. | ||
// Otherwise, it applies to the text to the left of it. | ||
return isFirstOnLine(text, lineStarts[curLine], pos) | ||
? curLine + 1 | ||
: curLine; | ||
} | ||
} | ||
|
||
function isFirstOnLine(text: string, lineStart: number, pos: number): boolean { | ||
for (let i = lineStart; i < pos; i++) { | ||
if (/\S/.test(text[i])) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { SyntaxError, TwoSlashAssertion } from "./types.js"; | ||
|
||
export function parseTwoslashAssertion( | ||
comment: string, | ||
commentIndex: number, | ||
commentLine: number, | ||
sourceText: string, | ||
lineStarts: readonly number[], | ||
): SyntaxError | TwoSlashAssertion | null { | ||
const matchTwoslash = /^( *)\^\?(.*)$/.exec(comment) as | ||
| [never, string, string] | ||
| null; | ||
if (!matchTwoslash) { | ||
return null; | ||
} | ||
|
||
const whitespace = matchTwoslash[1]; | ||
const rawPayload = matchTwoslash[2]; | ||
if (rawPayload.length && !rawPayload.startsWith(" ")) { | ||
// This is an error: there must be a space after the ^? | ||
return { | ||
line: commentLine - 1, | ||
type: "InvalidTwoslash", | ||
}; | ||
} | ||
|
||
let expected = rawPayload.slice(1); // strip leading space, or leave it as "". | ||
if (commentLine === 1) { | ||
// This will become an attachment error later. | ||
return { | ||
assertionType: "twoslash", | ||
expected, | ||
expectedPrefix: "", | ||
expectedRange: [-1, -1], | ||
insertSpace: false, | ||
position: -1, | ||
}; | ||
} | ||
|
||
// The position of interest is wherever the "^" (caret) is, but on the previous line. | ||
const caretIndex = commentIndex + whitespace.length + 2; // 2 = length of "//" | ||
const position = | ||
caretIndex - (lineStarts[commentLine - 1] - lineStarts[commentLine - 2]); | ||
|
||
const expectedRange: [number, number] = [ | ||
commentIndex + whitespace.length + 5, | ||
commentLine < lineStarts.length | ||
? lineStarts[commentLine] - 1 | ||
: sourceText.length, | ||
]; | ||
// Peak ahead to the next lines to see if the expected type continues | ||
const expectedPrefix = | ||
sourceText.slice( | ||
lineStarts[commentLine - 1], | ||
commentIndex + 2 + whitespace.length, | ||
) + " "; | ||
for (let nextLine = commentLine; nextLine < lineStarts.length; nextLine++) { | ||
const thisLineEnd = | ||
nextLine + 1 < lineStarts.length | ||
? lineStarts[nextLine + 1] - 1 | ||
: sourceText.length; | ||
const lineText = sourceText.slice(lineStarts[nextLine], thisLineEnd + 1); | ||
if (lineText.startsWith(expectedPrefix)) { | ||
if (nextLine === commentLine) { | ||
expected += "\n"; | ||
} | ||
|
||
expected += lineText.slice(expectedPrefix.length); | ||
expectedRange[1] = thisLineEnd; | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
let insertSpace = false; | ||
if (expectedRange[0] > expectedRange[1]) { | ||
// this happens if the line ends with "^?" and nothing else | ||
expectedRange[0] = expectedRange[1]; | ||
insertSpace = true; | ||
} | ||
|
||
return { | ||
assertionType: "twoslash", | ||
expected, | ||
expectedPrefix, | ||
expectedRange, | ||
insertSpace, | ||
position, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
export interface SyntaxError { | ||
readonly line: number; | ||
readonly type: | ||
| "InvalidTwoslash" | ||
| "MissingExpectType" | ||
| "MissingSnapshotName"; | ||
} | ||
|
||
export interface ManualAssertion { | ||
readonly assertionType: "manual"; | ||
readonly expected: string; | ||
} | ||
|
||
export interface SnapshotAssertion { | ||
readonly assertionType: "snapshot"; | ||
readonly expected: string | undefined; | ||
readonly snapshotName: string; | ||
} | ||
|
||
export interface TwoSlashAssertion { | ||
readonly assertionType: "twoslash"; | ||
|
||
/** | ||
* The expected type in the twoslash comment | ||
*/ | ||
readonly expected: string; | ||
|
||
/** | ||
* Text before the "^?" (used to produce continuation lines for fixer) | ||
*/ | ||
readonly expectedPrefix: string; | ||
|
||
/** | ||
* Range of positions corresponding to the "expected" string (for fixer) | ||
*/ | ||
readonly expectedRange: [number, number]; | ||
|
||
/** | ||
* Does a space need to be added after "^?" when fixing? (If "^?" ends the line.) | ||
*/ | ||
readonly insertSpace: boolean; | ||
|
||
/** | ||
* Position in the source file that the twoslash assertion points at | ||
*/ | ||
readonly position: number; | ||
} | ||
|
||
export type Assertion = ManualAssertion | SnapshotAssertion | TwoSlashAssertion; | ||
|
||
export interface Assertions { | ||
/** | ||
* Lines with more than one assertion (these are errors). | ||
*/ | ||
readonly duplicates: readonly number[]; | ||
|
||
/** | ||
* Lines with an $ExpectError. | ||
*/ | ||
readonly errorLines: ReadonlySet<number>; | ||
|
||
/** | ||
* Syntax Errors | ||
*/ | ||
readonly syntaxErrors: readonly SyntaxError[]; | ||
|
||
/** | ||
* Twoslash-style type assertions in the file | ||
*/ | ||
readonly twoSlashAssertions: readonly TwoSlashAssertion[]; | ||
|
||
/** | ||
* Map from a line number to the expected type at that line. | ||
*/ | ||
readonly typeAssertions: Map<number, Assertion>; | ||
} |
Oops, something went wrong.