Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

eslint-plugin-expect-type

Package Overview
Dependencies
Maintainers
3
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eslint-plugin-expect-type - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

dist/utils/diagnostics.js

309

dist/rules/expect.js

@@ -12,4 +12,4 @@ "use strict";

const snapshot_1 = require("../utils/snapshot");
const diagnostics_1 = require("../utils/diagnostics");
const messages = {
TypeScriptCompileError: 'TypeScript compile error: {{ message }}',
FileIsNotIncludedInTsconfig: 'Expected to find a file "{{ fileName }}" present.',

@@ -26,5 +26,2 @@ TypesDoNotMatch: 'Expected type to be: {{ expected }}, got: {{ actual }}',

const defaultOptions = {
// expectError: true,
// expectType: true,
// expectTypeSnapshot: true,
disableExpectTypeSnapshotFix: false,

@@ -37,11 +34,2 @@ };

properties: {
// expectError: {
// type: 'boolean',
// },
// expectType: {
// type: 'boolean',
// },
// expectTypeSnapshot: {
// type: 'boolean',
// },
disableExpectTypeSnapshotFix: {

@@ -92,13 +80,9 @@ type: 'boolean',

}
if (!/(?:\$Expect(Type|Error|^\?))|\^\?/.test(sourceFile.text)) {
return;
}
const checker = program.getTypeChecker();
// Don't care about emit errors.
const diagnostics = typescript_1.default.getPreEmitDiagnostics(program, sourceFile);
if (sourceFile.isDeclarationFile || !/\$Expect(Type|Error)/.test(sourceFile.text)) {
// Normal file.
for (const diagnostic of diagnostics) {
addDiagnosticFailure(diagnostic);
}
return;
}
const { errorLines, typeAssertions, duplicates, syntaxErrors } = parseAssertions(sourceFile);
const languageService = typescript_1.default.createLanguageService(getLanguageServiceHost(program));
const { errorLines, typeAssertions, twoSlashAssertions, duplicates, syntaxErrors } = parseAssertions(sourceFile);
for (const line of duplicates) {

@@ -113,10 +97,3 @@ context.report({

}
const seenDiagnosticsOnLine = new Set();
for (const diagnostic of diagnostics) {
const line = lineOfPosition(diagnostic.start, sourceFile);
seenDiagnosticsOnLine.add(line);
if (!errorLines.has(line)) {
addDiagnosticFailure(diagnostic);
}
}
const seenDiagnosticsOnLine = new Set(diagnostics.filter(diagnostics_1.isDiagnosticWithStart).map((diagnostic) => lineOfPosition(diagnostic.start, sourceFile)));
for (const line of errorLines) {

@@ -139,3 +116,5 @@ if (!seenDiagnosticsOnLine.has(line)) {

? '$ExpectType requires type argument (e.g. // $ExpectType "string")'
: '$ExpectTypeSnapshot requires snapshot name argument (e.g. // $ExpectTypeSnapshot MainComponentAPI)',
: type === 'MissingSnapshotName'
? '$ExpectTypeSnapshot requires snapshot name argument (e.g. // $ExpectTypeSnapshot MainComponentAPI)'
: 'Invalid twoslash assertion; make sure there is a space after the "^?".',
},

@@ -153,3 +132,3 @@ loc: {

}
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, typeAssertions, checker);
const { unmetExpectations, unusedAssertions } = getExpectTypeFailures(sourceFile, { typeAssertions, twoSlashAssertions }, checker, languageService);
for (const { node, assertion, actual } of unmetExpectations) {

@@ -193,3 +172,17 @@ const templateDescriptor = {

else {
context.report(Object.assign(Object.assign({}, templateDescriptor), { messageId: 'TypesDoNotMatch' }));
context.report(Object.assign(Object.assign(Object.assign({}, templateDescriptor), { messageId: 'TypesDoNotMatch' }), (assertion.assertionType === 'twoslash'
? {
fix: () => {
const { expectedRange, expectedPrefix, insertSpace } = assertion;
return {
range: expectedRange,
text: (insertSpace ? ' ' : '') +
actual
.split('\n')
.map((line, i) => (i > 0 ? expectedPrefix + line : line))
.join('\n'),
};
},
}
: {})));
}

@@ -206,36 +199,2 @@ }

}
function diagnosticShouldBeIgnored(diagnostic) {
const messageText = typeof diagnostic.messageText === 'string' ? diagnostic.messageText : diagnostic.messageText.messageText;
return /'.+' is declared but (never used|its value is never read)./.test(messageText);
}
function addDiagnosticFailure(diagnostic) {
if (diagnosticShouldBeIgnored(diagnostic)) {
return;
}
if (diagnostic.file === sourceFile) {
const message = `${typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`;
context.report({
messageId: 'TypeScriptCompileError',
data: {
message,
},
loc: {
line: diagnostic.start + 1,
column: diagnostic.length,
},
});
}
else {
context.report({
messageId: 'TypeScriptCompileError',
data: {
message: `${fileName}${diagnostic.messageText}`,
},
loc: {
line: 1,
column: 0,
},
});
}
}
}

@@ -247,2 +206,3 @@ function parseAssertions(sourceFile) {

const syntaxErrors = [];
const twoSlashAssertions = [];
const { text } = sourceFile;

@@ -259,55 +219,71 @@ const commentRegexp = /\/\/(.*)/g;

// i.e. `// foo; // $ExpectType number`
const match = /^ ?\$Expect(TypeSnapshot|Type|Error)( (.*))?$/.exec(commentMatch[1]);
if (match === null) {
continue;
}
const line = getLine(commentMatch.index);
switch (match[1]) {
case 'TypeSnapshot':
const snapshotName = match[3];
if (snapshotName) {
if (typeAssertions.delete(line)) {
duplicates.push(line);
const comment = commentMatch[1];
const matchExpect = /^ ?\$Expect(TypeSnapshot|Type|Error)( (.*))?$/.exec(comment);
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',
snapshotName,
});
}
}
else {
typeAssertions.set(line, {
assertionType: 'snapshot',
snapshotName,
syntaxErrors.push({
type: 'MissingSnapshotName',
line,
});
}
}
else {
syntaxErrors.push({
type: 'MissingSnapshotName',
line,
});
}
break;
case 'Error':
if (errorLines.has(line)) {
duplicates.push(line);
}
errorLines.add(line);
break;
case 'Type':
const expected = match[3];
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)) {
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 {
typeAssertions.set(line, { assertionType: 'manual', expected });
syntaxErrors.push({
type: 'MissingExpectType',
line,
});
}
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 {
syntaxErrors.push({
type: 'MissingExpectType',
line,
});
twoSlashAssertions.push(assertion);
}
break;
}
}
}
return { errorLines, typeAssertions, duplicates, syntaxErrors };
return { errorLines, typeAssertions, duplicates, twoSlashAssertions, syntaxErrors };
function getLine(pos) {

@@ -323,2 +299,58 @@ // advance curLine to be the line preceding 'pos'

}
function parseTwoslashAssertion(comment, commentIndex, commentLine, sourceText, lineStarts) {
const matchTwoslash = /^( *)\^\?(.*)$/.exec(comment);
if (!matchTwoslash) {
return null;
}
const whitespace = matchTwoslash[1];
const rawPayload = matchTwoslash[2];
if (rawPayload.length && rawPayload[0] !== ' ') {
// This is an error: there must be a space after the ^?
return {
type: 'InvalidTwoslash',
line: commentLine - 1,
};
}
let expected = rawPayload.slice(1); // strip leading space, or leave it as "".
if (commentLine === 1) {
// This will become an attachment error later.
return {
position: -1,
expected,
expectedRange: [-1, -1],
expectedPrefix: '',
insertSpace: false,
};
}
// 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 = [
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 { position, expected, expectedRange, expectedPrefix, insertSpace };
}
function isFirstOnLine(text, lineStart, pos) {

@@ -374,3 +406,14 @@ for (let i = lineStart; i < pos; i++) {

}
function getExpectTypeFailures(sourceFile, typeAssertions, checker) {
function getLanguageServiceHost(program) {
return {
getCompilationSettings: () => program.getCompilerOptions(),
getCurrentDirectory: () => program.getCurrentDirectory(),
getDefaultLibFileName: () => 'lib.d.ts',
getScriptFileNames: () => program.getSourceFiles().map((sourceFile) => sourceFile.fileName),
getScriptSnapshot: (name) => { var _a, _b; return typescript_1.default.ScriptSnapshot.fromString((_b = (_a = program.getSourceFile(name)) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : ''); },
getScriptVersion: () => '1',
};
}
function getExpectTypeFailures(sourceFile, assertions, checker, languageService) {
const { typeAssertions, twoSlashAssertions } = assertions;
const unmetExpectations = [];

@@ -384,2 +427,3 @@ // Match assertions to the first node that appears on the line they apply to.

const { expected } = assertion;
let nodeToCheck = node;
// https://github.com/Microsoft/TypeScript/issues/14077

@@ -389,3 +433,4 @@ if (node.kind === typescript_1.default.SyntaxKind.ExpressionStatement) {

}
const type = checker.getTypeAtLocation(getNodeForExpectType(node));
nodeToCheck = getNodeForExpectType(node);
const type = checker.getTypeAtLocation(nodeToCheck);
const actual = type

@@ -401,4 +446,48 @@ ? checker.typeToString(type, /*enclosingDeclaration*/ undefined, typescript_1.default.TypeFormatFlags.NoTruncation)

});
return { unmetExpectations, unusedAssertions: typeAssertions.keys() };
const twoSlashFailureLines = [];
if (twoSlashAssertions.length) {
for (const assertion of twoSlashAssertions) {
const { position, expected } = assertion;
if (position === -1) {
// special case for a twoslash assertion on line 1.
twoSlashFailureLines.push(0);
continue;
}
const node = getNodeAtPosition(sourceFile, position);
if (!node) {
twoSlashFailureLines.push(sourceFile.getLineAndCharacterOfPosition(position).line);
continue;
}
const qi = languageService.getQuickInfoAtPosition(sourceFile.fileName, node.getStart());
if (!(qi === null || qi === void 0 ? void 0 : qi.displayParts)) {
twoSlashFailureLines.push(sourceFile.getLineAndCharacterOfPosition(position).line);
continue;
}
const actual = qi.displayParts.map((dp) => dp.text).join('');
if (!matchModuloWhitespace(actual, expected)) {
unmetExpectations.push({ assertion: Object.assign({ assertionType: 'twoslash' }, assertion), node, actual });
}
}
}
return { unmetExpectations, unusedAssertions: [...twoSlashFailureLines, ...typeAssertions.keys()] };
}
function getNodeAtPosition(sourceFile, position) {
let candidate = undefined;
typescript_1.default.forEachChild(sourceFile, function iterate(node) {
const start = node.getStart();
const end = node.getEnd();
if (position >= start && position <= end) {
candidate = node;
typescript_1.default.forEachChild(node, iterate);
}
});
return candidate;
}
function matchModuloWhitespace(actual, expected) {
// TODO: it's much easier to normalize actual based on the displayParts
// This isn't 100% correct if a type has a space in it, e.g. type T = "string literal"
const normActual = actual.replace(/[\n ]+/g, ' ').trim();
const normExpected = expected.replace(/[\n ]+/g, ' ').trim();
return normActual === normExpected;
}
function getNodeForExpectType(node) {

@@ -405,0 +494,0 @@ if (node.kind === typescript_1.default.SyntaxKind.VariableStatement) {

{
"name": "eslint-plugin-expect-type",
"version": "0.1.0",
"version": "0.2.0",
"description": "ESLint plugin with $ExpectType, $ExpectError and $ExpectTypeSnapshot type assertions",

@@ -5,0 +5,0 @@ "author": {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc