@fimbul/mimir
Advanced tools
Comparing version 0.21.0-dev.20190322 to 0.21.0-dev.20190329
{ | ||
"name": "@fimbul/mimir", | ||
"version": "0.21.0-dev.20190322", | ||
"version": "0.21.0-dev.20190329", | ||
"description": "Core rules of the Fimbullinter project", | ||
@@ -28,3 +28,3 @@ "main": "recommended.yaml", | ||
"dependencies": { | ||
"@fimbul/ymir": "0.21.0-dev.20190322", | ||
"@fimbul/ymir": "0.21.0-dev.20190329", | ||
"chalk": "^2.3.2", | ||
@@ -31,0 +31,0 @@ "debug": "^4.0.0", |
@@ -40,3 +40,3 @@ "use strict"; | ||
if (line !== 0 || character === 0 || !summary.content.startsWith('\uFEFF')) | ||
character += 1; | ||
character += 1; // avoid incrementing the character position on the first line if BOM is present, editors ignore BOM | ||
const position = `${line + 1}:${character}`; | ||
@@ -86,3 +86,3 @@ if (position.length > this.maxPositionWidth) | ||
? undefined | ||
: lines.slice(1).join('\n'); | ||
: lines.slice(1).join('\n'); // remove first line, because it's always empty | ||
} | ||
@@ -89,0 +89,0 @@ } |
@@ -5,6 +5,15 @@ import { TypedRule } from '@fimbul/ymir'; | ||
private maybePromiseLike; | ||
/** | ||
* A type is thenable when it has a callable `then` property. | ||
* We don't care if the signatures are actually callable because the compiler already complains about that. | ||
*/ | ||
private isThenable; | ||
private isAsyncIterable; | ||
/** | ||
* We consider a type as AsyncIterable when it has a property key [Symbol.asyncIterator]. | ||
* The spec requires this property to be a function returning an object with a `next` method which returns a Promise of IteratorResult. | ||
* But that's none of our business as the compiler already does the heavy lifting. | ||
*/ | ||
private hasSymbolAsyncIterator; | ||
private isIterableOfPromises; | ||
} |
@@ -41,2 +41,6 @@ "use strict"; | ||
} | ||
/** | ||
* A type is thenable when it has a callable `then` property. | ||
* We don't care if the signatures are actually callable because the compiler already complains about that. | ||
*/ | ||
isThenable(type, node) { | ||
@@ -51,2 +55,3 @@ const then = type.getProperty('then'); | ||
for (const t of tsutils_1.unionTypeParts(type)) | ||
// there must either be a `AsyncIterable` or `Iterable<PromiseLike<any>>` | ||
if (this.hasSymbolAsyncIterator(t) || this.isIterableOfPromises(t, node)) | ||
@@ -56,2 +61,7 @@ return true; | ||
} | ||
/** | ||
* We consider a type as AsyncIterable when it has a property key [Symbol.asyncIterator]. | ||
* The spec requires this property to be a function returning an object with a `next` method which returns a Promise of IteratorResult. | ||
* But that's none of our business as the compiler already does the heavy lifting. | ||
*/ | ||
hasSymbolAsyncIterator(type) { | ||
@@ -58,0 +68,0 @@ return type.getProperties().some((prop) => prop.escapedName === '__@asyncIterator'); |
@@ -60,2 +60,3 @@ "use strict"; | ||
function isWhitelisted(name) { | ||
// exclude all identifiers that start with an uppercase letter to allow `new Event()` and stuff | ||
if (name.charAt(0) === name.charAt(0).toUpperCase()) | ||
@@ -62,0 +63,0 @@ return true; |
@@ -16,9 +16,9 @@ "use strict"; | ||
iterate(wrap, end) { | ||
do { | ||
do { // iterate as linked list until we find a generator | ||
if (wrap.kind === ts.SyntaxKind.Block && isGenerator(wrap.node.parent)) { | ||
this.containsYield = false; | ||
wrap.children.forEach(this.visitNode, this); | ||
if (this.shouldFail()) | ||
wrap.children.forEach(this.visitNode, this); // walk the function body recursively | ||
if (this.shouldFail()) // call as method so CFA doesn't infer `this.containsYield` as always false | ||
this.fail(wrap.node.parent); | ||
wrap = wrap.skip; | ||
wrap = wrap.skip; // continue right after the function body | ||
} | ||
@@ -39,2 +39,3 @@ else { | ||
const saved = this.containsYield; | ||
// can iterate as linked list again for nested functions | ||
this.iterate(wrap.next, wrap.skip); | ||
@@ -41,0 +42,0 @@ this.containsYield = saved; |
@@ -36,2 +36,3 @@ "use strict"; | ||
default: | ||
// union of literal types, do not add these to `valuesSeen`, but display an error if all literals were already seen | ||
if (literals.every((v) => valuesSeen.has(v))) | ||
@@ -59,3 +60,3 @@ this.addFindingAtNode(clause.expression, `Duplicate 'case ${literals.sort().join(' | ')}'.`); | ||
if (node.kind === ts.SyntaxKind.NullKeyword) | ||
return [formatPrimitive(prefixFn(null))]; | ||
return [formatPrimitive(prefixFn(null))]; // tslint:disable-line:no-null-keyword | ||
if (tsutils_1.isIdentifier(node) && node.originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) | ||
@@ -74,2 +75,3 @@ return [formatPrimitive(prefixFn(undefined))]; | ||
for (const t of tsutils_1.unionTypeParts(type)) { | ||
// TODO handle intersection types | ||
if (tsutils_1.isLiteralType(t)) { | ||
@@ -85,3 +87,3 @@ result.add(formatPrimitive(prefixFn(t.value))); | ||
else if (t.flags & ts.TypeFlags.Null) { | ||
result.add(formatPrimitive(prefixFn(null))); | ||
result.add(formatPrimitive(prefixFn(null))); // tslint:disable-line:no-null-keyword | ||
} | ||
@@ -107,2 +109,3 @@ else { | ||
case ts.SyntaxKind.MinusToken: | ||
// there's no '-0n' | ||
return (v) => isBigInt(v) ? next(Object.assign({}, v, { negative: !v.negative && v.base10Value !== '0' })) : next(-v); | ||
@@ -123,2 +126,3 @@ case ts.SyntaxKind.TildeToken: | ||
if (v.negative) { | ||
// negative values become positive and get decremented by 1 | ||
for (let i = digits.length - 1; i >= 0; --i) { | ||
@@ -128,2 +132,3 @@ const current = +digits[i] - 1; | ||
if (current === 0 && i === 0 && digits.length !== 1) { | ||
// remove leading zero | ||
digits.shift(); | ||
@@ -140,2 +145,3 @@ } | ||
else { | ||
// positive values are incremented by one and become negative | ||
for (let i = digits.length - 1; i >= 0; --i) { | ||
@@ -142,0 +148,0 @@ const current = +digits[i] + 1; |
@@ -26,2 +26,3 @@ "use strict"; | ||
checkObject({ properties }) { | ||
/** key: propertyName, value: isAccessor */ | ||
const propertiesSeen = new Map(); | ||
@@ -39,3 +40,3 @@ for (let i = properties.length - 1; i >= 0; --i) { | ||
if (isAccessor) | ||
continue; | ||
continue; // avoid overriding the isAccessor state | ||
} | ||
@@ -42,0 +43,0 @@ } |
import { TypedRule } from '@fimbul/ymir'; | ||
export declare class Rule extends TypedRule { | ||
apply(): void; | ||
/** | ||
* This function is necessary because higher order function type inference creates Signatures whose declaration has no type parameters. | ||
* @see https://github.com/Microsoft/TypeScript/issues/30296 | ||
* | ||
* To work around this, we look for a single call signature on the called expression and use its type parameters instead. | ||
* As a bonus this also works for unified signatures from union types. | ||
*/ | ||
private getTypeParametersOfCallSignature; | ||
@@ -5,0 +12,0 @@ private mapTypeParameter; |
@@ -26,2 +26,9 @@ "use strict"; | ||
} | ||
/** | ||
* This function is necessary because higher order function type inference creates Signatures whose declaration has no type parameters. | ||
* @see https://github.com/Microsoft/TypeScript/issues/30296 | ||
* | ||
* To work around this, we look for a single call signature on the called expression and use its type parameters instead. | ||
* As a bonus this also works for unified signatures from union types. | ||
*/ | ||
getTypeParametersOfCallSignature(node) { | ||
@@ -40,2 +47,5 @@ let expr; | ||
const signatures = this.checker.getTypeAtLocation(expr).getCallSignatures(); | ||
// abort if not all signatures have type parameters: | ||
// higher order function type inference only works for a single call signature | ||
// call signature unification puts type parameters on every resulting signature | ||
if (signatures.length === 0 || signatures.some((s) => s.typeParameters === undefined)) | ||
@@ -46,2 +56,3 @@ return []; | ||
mapTypeParameter(type) { | ||
// fall back to NodeBuilder for renamed TypeParameters, they have no declaration and therefore we cannot directly access the default | ||
return type.symbol.declarations === undefined | ||
@@ -67,11 +78,14 @@ ? mapTypeParameterDeclaration(this.checker.typeParameterToDeclaration(type, undefined, ts.NodeBuilderFlags.IgnoreErrors)) | ||
if (signature.declaration !== undefined) { | ||
// There is an explicitly declared construct signature | ||
const typeParameters = ts.getEffectiveTypeParameterDeclarations(signature.declaration); | ||
if (typeParameters.length !== 0) | ||
if (typeParameters.length !== 0) // only check the signature if it declares type parameters | ||
return this.checkInferredTypeParameters(signature, typeParameters.map(mapTypeParameterDeclaration), node); | ||
if (signature.declaration.kind !== ts.SyntaxKind.Constructor) | ||
return; | ||
return; // don't look for type parameters on non-class parents | ||
} | ||
// Otherwise look up the TypeParameters of the ClassDeclaration | ||
const { symbol } = this.checker.getTypeAtLocation(node.expression); | ||
if (symbol === undefined || symbol.declarations === undefined) | ||
return; | ||
// collect all TypeParameters and their defaults from all merged declarations | ||
const mergedTypeParameters = []; | ||
@@ -92,3 +106,3 @@ for (const declaration of symbol.declarations) { | ||
if (typeParameters.every((t) => t.hasDefault)) | ||
return; | ||
return; // nothing to do here if every type parameter as a default | ||
const typeArguments = this.checker.signatureToSignatureDeclaration(signature, ts.SyntaxKind.CallExpression, undefined, ts.NodeBuilderFlags.WriteTypeArgumentsOfSignature | ts.NodeBuilderFlags.IgnoreErrors).typeArguments; | ||
@@ -106,3 +120,3 @@ for (let i = 0; i < typeParameters.length; ++i) { | ||
ymir_1.excludeDeclarationFiles, | ||
ymir_1.typescriptOnly | ||
ymir_1.typescriptOnly // in .js files TypeParameters default to `any` | ||
], Rule); | ||
@@ -109,0 +123,0 @@ exports.Rule = Rule; |
@@ -26,2 +26,3 @@ "use strict"; | ||
return; | ||
// if expression is a type variable, the type checker already handles everything as expected | ||
const originalTypeParts = getLiteralsByType(this.checker.getTypeAtLocation(node.expression)); | ||
@@ -81,2 +82,6 @@ if (isEmpty(originalTypeParts)) | ||
}; | ||
// typically literal types are swallowed by their corresponding widened type if they occur in the same union | ||
// this is not the case with intersections: `(string & {foo: string}) | ('bar' & {bar: string})` | ||
// therefore we need to reset all previously seen literal types if we see the widened type | ||
// we also need to remember not to store any new literal types of that kind | ||
let seenString = false; | ||
@@ -83,0 +88,0 @@ let seenNumber = false; |
@@ -12,3 +12,3 @@ "use strict"; | ||
for (let match = re.exec(this.sourceFile.text); match !== null; match = re.exec(this.sourceFile.text)) { | ||
if (match[1].length & 1) | ||
if (match[1].length & 1) // only check if backslash is not escaped | ||
continue; | ||
@@ -15,0 +15,0 @@ const { node } = tsutils_1.getWrappedNodeAtPosition(wrappedAst || (wrappedAst = this.context.getWrappedAst()), match.index); |
@@ -168,2 +168,3 @@ "use strict"; | ||
case ts.SyntaxKind.Decorator: | ||
// skip the declaration the decorator belongs to, because decorators are always applied in the outer context | ||
node = node.parent.parent; | ||
@@ -170,0 +171,0 @@ break; |
@@ -62,7 +62,13 @@ "use strict"; | ||
if (tsutils_1.isFunctionScopeBoundary(node)) | ||
return false; | ||
return false; // stop at function boundaries | ||
if (tsutils_1.isTryStatement(node.parent)) { | ||
if (node.parent.tryBlock === node || | ||
if ( | ||
// statements inside the try block always have an error handler, either catch or finally | ||
node.parent.tryBlock === node || | ||
// return await inside the catch block is allowed if there is a finally block | ||
// otherwise the finally block is executed before the promise returned from catch resolves | ||
node.parent.finallyBlock !== undefined && node.parent.catchClause === node) | ||
return true; | ||
// we know we can skip over the TryStatement, it always has a parent | ||
// if it's a function or the SourceFile, we stop. or we may potentially land in another (try/catch) block | ||
node = node.parent.parent; | ||
@@ -69,0 +75,0 @@ } |
@@ -80,2 +80,3 @@ "use strict"; | ||
case ts.SyntaxKind.DoStatement: | ||
// statements after loops, that go on forever without breaking, are never executed | ||
return getConstantIterationCondition(statement) === true && | ||
@@ -82,0 +83,0 @@ !tsutils_1.getControlFlowEnd(statement.statement).statements.some((jump) => jump.kind === ts.SyntaxKind.BreakStatement && (jump.label === undefined || labels.includes(jump.label.text))); |
@@ -12,2 +12,3 @@ "use strict"; | ||
for (const node of this.context.getFlatAst()) { | ||
// TODO maybe check Type["property"] | ||
if (tsutils_1.isIdentifier(node)) { | ||
@@ -164,2 +165,3 @@ if (shouldCheckIdentifier(node)) | ||
case ts.SyntaxKind.CallExpression: | ||
// note: NewExpression will never get here, because if the class is deprecated, we show an error all the time | ||
return parent.expression === node; | ||
@@ -184,3 +186,3 @@ case ts.SyntaxKind.JsxOpeningElement: | ||
return false; | ||
case ts.SyntaxKind.ShorthandPropertyAssignment: | ||
case ts.SyntaxKind.ShorthandPropertyAssignment: // checked separately | ||
return node.parent.name !== node; | ||
@@ -192,4 +194,7 @@ default: | ||
function shouldCheckQualifiedName(node) { | ||
// if parent is a QualifiedName, it is the my.ns part of my.ns.Something -> we definitely want to check that | ||
// if the parent is an ImportEqualsDeclaration -> we don't want to check the rightmost identifier, because importing is not that bad | ||
// everything else is a TypeReference -> we want to check that | ||
return node.parent.kind !== ts.SyntaxKind.ImportEqualsDeclaration; | ||
} | ||
//# sourceMappingURL=no-unstable-api-use.js.map |
@@ -20,2 +20,3 @@ "use strict"; | ||
else if (tsutils_1.isExpressionStatement(node)) { | ||
// allow `void asyncFn()` to mute 'await-async-result' | ||
if (!isDirective(node) && node.expression.kind !== ts.SyntaxKind.VoidExpression) | ||
@@ -45,3 +46,3 @@ this.checkNode(node.expression, node); | ||
case ts.SyntaxKind.DeleteExpression: | ||
case ts.SyntaxKind.PostfixUnaryExpression: | ||
case ts.SyntaxKind.PostfixUnaryExpression: // i++, i-- | ||
return true; | ||
@@ -89,2 +90,6 @@ case ts.SyntaxKind.PrefixUnaryExpression: | ||
exports.Rule = Rule; | ||
/** | ||
* A directive is an expression statement containing only a string literal. | ||
* Directives need to be located at the beginning of a function block and be consecutive. | ||
*/ | ||
function isDirective(statement) { | ||
@@ -102,2 +107,3 @@ if (statement.expression.kind !== ts.SyntaxKind.StringLiteral) | ||
} | ||
/** `void 0` and `void(0)` are allowed to have no side effect. */ | ||
function isAllowedVoidExpression(expr) { | ||
@@ -108,2 +114,3 @@ if (expr.kind === ts.SyntaxKind.ParenthesizedExpression) | ||
} | ||
/** Allow `(0, eval)('foo')` */ | ||
function isIndirectEval(node) { | ||
@@ -110,0 +117,0 @@ return tsutils_1.isIdentifier(node.right) && node.right.text === 'eval' && |
@@ -10,3 +10,4 @@ import { TypedRule } from '@fimbul/ymir'; | ||
private reportUselessTypeAssertion; | ||
/** Returns the contextual type if it is a position that does not contribute to control flow analysis. */ | ||
private getSafeContextualType; | ||
} |
@@ -36,13 +36,17 @@ "use strict"; | ||
checkDefiniteAssignmentAssertion(node) { | ||
// compiler already emits an error for definite assignment assertions on ambient or initialized variables | ||
if (node.exclamationToken !== undefined && | ||
node.initializer === undefined && (!tsutils_1.isStrictCompilerOptionEnabled(this.context.compilerOptions, 'strictNullChecks') || | ||
getNullableFlags(this.checker.getTypeAtLocation(node.name), true) & ts.TypeFlags.Undefined)) | ||
getNullableFlags(this.checker.getTypeAtLocation(node.name), true) & ts.TypeFlags.Undefined // type does not allow undefined | ||
)) | ||
this.addFinding(node.exclamationToken.end - 1, node.exclamationToken.end, FAIL_DEFINITE_ASSIGNMENT, ymir_1.Replacement.delete(node.exclamationToken.pos, node.exclamationToken.end)); | ||
} | ||
checkDefiniteAssignmentAssertionProperty(node) { | ||
// compiler emits an error for definite assignment assertions on ambient, initialized or abstract properties | ||
if (node.exclamationToken !== undefined && | ||
node.initializer === undefined && | ||
!tsutils_1.hasModifier(node.modifiers, ts.SyntaxKind.AbstractKeyword) && (node.name.kind !== ts.SyntaxKind.Identifier || | ||
!tsutils_1.hasModifier(node.modifiers, ts.SyntaxKind.AbstractKeyword) && (node.name.kind !== ts.SyntaxKind.Identifier || // properties with string or computed name are not checked | ||
!tsutils_1.isStrictCompilerOptionEnabled(this.context.compilerOptions, 'strictPropertyInitialization') || | ||
getNullableFlags(this.checker.getTypeAtLocation(node), true) & ts.TypeFlags.Undefined)) | ||
getNullableFlags(this.checker.getTypeAtLocation(node), true) & ts.TypeFlags.Undefined // type does not allow undefined | ||
)) | ||
this.addFinding(node.exclamationToken.end - 1, node.exclamationToken.end, FAIL_DEFINITE_ASSIGNMENT, ymir_1.Replacement.delete(node.exclamationToken.pos, node.exclamationToken.end)); | ||
@@ -55,3 +59,3 @@ } | ||
const flags = getNullableFlags(this.checker.getBaseConstraintOfType(originalType) || originalType); | ||
if (flags !== 0) { | ||
if (flags !== 0) { // type is nullable | ||
const contextualType = this.getSafeContextualType(node); | ||
@@ -76,3 +80,3 @@ if (contextualType === undefined || (flags & ~getNullableFlags(contextualType, true))) | ||
let targetType = this.checker.getTypeFromTypeNode(node.type); | ||
if ((targetType.flags & ts.TypeFlags.Literal) !== 0 && !tsutils_1.isInConstContext(node) || | ||
if ((targetType.flags & ts.TypeFlags.Literal) !== 0 && !tsutils_1.isInConstContext(node) || // allow "foo" as "foo" to avoid widening | ||
tsutils_1.isObjectType(targetType) && (targetType.objectFlags & ts.ObjectFlags.Tuple || couldBeTupleType(targetType))) | ||
@@ -88,6 +92,9 @@ return; | ||
const contextualType = this.getSafeContextualType(node); | ||
// TODO use assignability check once it's exposed from TypeChecker | ||
if (contextualType === undefined || | ||
contextualType.flags & (ts.TypeFlags.TypeVariable | ts.TypeFlags.Instantiable) || | ||
(contextualType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0 && | ||
// contextual type is exactly the same | ||
!utils_1.typesAreEqual(sourceType, contextualType, this.checker) && | ||
// contextual type is an optional parameter or similar | ||
!utils_1.typesAreEqual(sourceType, tsutils_1.removeOptionalityFromType(this.checker, contextualType), this.checker)) | ||
@@ -111,4 +118,7 @@ return; | ||
} | ||
/** Returns the contextual type if it is a position that does not contribute to control flow analysis. */ | ||
getSafeContextualType(node) { | ||
const parent = node.parent; | ||
// If assertion is used as argument, check if the function accepts this expression without an assertion | ||
// TODO expand this to VariableLike initializers and return expressions where a type declaration exists | ||
switch (parent.kind) { | ||
@@ -120,3 +130,3 @@ case ts.SyntaxKind.CallExpression: | ||
break; | ||
case ts.SyntaxKind.TemplateSpan: | ||
case ts.SyntaxKind.TemplateSpan: // TODO return 'any' for non-tagged template expressions | ||
case ts.SyntaxKind.JsxExpression: | ||
@@ -148,2 +158,19 @@ break; | ||
} | ||
/** | ||
* Determine if the assertion may be necessary to avoid a compile error for use before being assigned. | ||
* This function may yield some false negatives, but is needed until https://github.com/Microsoft/TypeScript/issues/20221 is implemented. | ||
* | ||
* The rules are as follows: | ||
* * strictNullChecks are enabled | ||
* * assertion is on an identifier | ||
* * identifier is a mutable variable | ||
* * no destructuring, parameters, catch binding, ambient declarations | ||
* * variable is not initialized | ||
* * variable has no definite assignment assertion (exclamation mark) | ||
* * variable has a type annotation | ||
* * declared type is equal to the type at the assertion | ||
* * declaration and assertion are in the same function scope | ||
* | ||
* We don't need to worry about strictPropertyInitialization errors, because they cannot be suppressed with a non-null assertion | ||
*/ | ||
function maybeUsedBeforeBeingAssigned(node, type, checker) { | ||
@@ -167,5 +194,7 @@ if (node.kind !== ts.SyntaxKind.Identifier || getNullableFlags(type, true) & ts.TypeFlags.Undefined) | ||
while (useFunctionScope !== declaringFunctionScope && isInlinedIife(useFunctionScope)) | ||
// TypeScript inlines IIFEs, so we need to do as well | ||
useFunctionScope = findupFunction(useFunctionScope.parent.parent); | ||
return useFunctionScope === declaringFunctionScope; | ||
} | ||
/** Finds the nearest parent that has a function scope. */ | ||
function findupFunction(node) { | ||
@@ -178,6 +207,10 @@ while (!tsutils_1.isFunctionScopeBoundary(node) && node.kind !== ts.SyntaxKind.SourceFile) | ||
return (tsutils_1.isFunctionExpression(node) || tsutils_1.isArrowFunction(node)) && | ||
node.asteriskToken === undefined && | ||
!tsutils_1.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword) && | ||
node.asteriskToken === undefined && // exclude generators | ||
!tsutils_1.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword) && // exclude async functions | ||
tsutils_1.getIIFE(node) !== undefined; | ||
} | ||
/** | ||
* Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type. | ||
* So, in addition, check if there are integer properties 0..n and no other numeric keys | ||
*/ | ||
function couldBeTupleType(type) { | ||
@@ -192,2 +225,3 @@ const properties = type.getProperties(); | ||
if (i === 0) | ||
// if there are no integer properties, this is not a tuple | ||
return false; | ||
@@ -194,0 +228,0 @@ break; |
@@ -38,2 +38,3 @@ "use strict"; | ||
case ts.SyntaxKind.EnumDeclaration: | ||
// allow 'declare const enum' in declaration files, because it's required in declaration files | ||
return !this.sourceFile.isDeclarationFile && tsutils_1.hasModifier(node.modifiers, ts.SyntaxKind.ConstKeyword); | ||
@@ -40,0 +41,0 @@ default: |
@@ -34,3 +34,3 @@ "use strict"; | ||
if (isRestProperty(properties[properties.length - 1])) | ||
return; | ||
return; // don't report empty property destructuring if there is a rest property as it's used to exclude properties | ||
for (let i = 0;; ++i) { | ||
@@ -40,3 +40,5 @@ const isLast = i === properties.length - 1; | ||
if (!isUsed(property)) | ||
this.addFindingAtNode(property, "Destructuring property is not necessary as it doesn't assign to a variable.", ymir_1.Replacement.delete(property.pos, isLast ? properties.end : properties[i + 1].pos)); | ||
this.addFindingAtNode(property, "Destructuring property is not necessary as it doesn't assign to a variable.", | ||
// make sure to delete the next comma to avoid consecutive commas | ||
ymir_1.Replacement.delete(property.pos, isLast ? properties.end : properties[i + 1].pos)); | ||
if (isLast) | ||
@@ -99,5 +101,5 @@ break; | ||
switch (node.kind) { | ||
case ts.SyntaxKind.OmittedExpression: | ||
case ts.SyntaxKind.OmittedExpression: // only possible in array destructuring | ||
return false; | ||
case ts.SyntaxKind.SpreadElement: | ||
case ts.SyntaxKind.SpreadElement: // only possible in array destructuring | ||
return isNonEmptyAssignmentTarget(node.expression); | ||
@@ -104,0 +106,0 @@ case ts.SyntaxKind.ObjectLiteralExpression: |
@@ -75,2 +75,5 @@ "use strict"; | ||
continue; | ||
// TODO we currently cannot autofix this case: it's possible to use a default value that's not assignable to the | ||
// destructured type. The type of the variable then includes the type of the initializer as well. | ||
// Removing the initializer might also remove its type from the union type causing type errors elsewhere. | ||
this.addFindingAtNode(element.initializer, "Unnecessary default value as this property is never 'undefined'."); | ||
@@ -77,0 +80,0 @@ } |
@@ -32,15 +32,16 @@ "use strict"; | ||
if (tsutils_1.isIterationStatement(parent) || isBreak && parent.kind === ts.SyntaxKind.SwitchStatement) { | ||
// we found the closest jump target. now we need to check if it has the same label as the jump statement | ||
parent = parent.parent; | ||
while (tsutils_1.isLabeledStatement(parent)) { | ||
if (parent.label.text === label.text) | ||
return false; | ||
return false; // label is present on the closest jump target | ||
parent = parent.parent; | ||
} | ||
return true; | ||
return true; // label is not present on the closest jump target | ||
} | ||
if (isBreak && tsutils_1.isLabeledStatement(parent) && parent.label.text === label.text) | ||
return true; | ||
return true; // label is not on an IterationStatement or SwitchStatement -> breaking out of blocks always requires label | ||
} while (!tsutils_1.isFunctionScopeBoundary(parent) && parent.kind !== ts.SyntaxKind.SourceFile); | ||
return true; | ||
return true; // label is not in scope, should never get here in correct code | ||
} | ||
//# sourceMappingURL=no-useless-jump-label.js.map |
@@ -15,2 +15,7 @@ import { TypedRule } from '@fimbul/ymir'; | ||
private executePredicate; | ||
/** | ||
* If one of the types in the intersection matches the precicate, this function returns true. | ||
* Otherwise if any of the types results in undefined, this function returns undefined. | ||
* If every type results in false, it returns false. | ||
*/ | ||
private matchIntersectionType; | ||
@@ -17,0 +22,0 @@ private getTypeOfExpression; |
@@ -115,3 +115,3 @@ "use strict"; | ||
if (tsutils_1.isBinaryExpression(node)) { | ||
if (isEqualityOperator(node.operatorToken.kind)) | ||
if (isEqualityOperator(node.operatorToken.kind)) // TODO check <, >, <=, >= with literal types | ||
return nested ? undefined : this.isConstantComparison(node.left, node.right, node.operatorToken.kind); | ||
@@ -127,2 +127,3 @@ if (node.operatorToken.kind === ts.SyntaxKind.InKeyword) | ||
} | ||
// in non-strictNullChecks mode we can only detect if a type is definitely falsy | ||
return this.executePredicate(this.getTypeOfExpression(node), this.strictNullChecks ? truthyFalsy : falsy); | ||
@@ -189,2 +190,3 @@ } | ||
getPrimitiveLiteral(node) { | ||
// TODO reuse some logic from 'no-duplicate-case' to compute prefix unary expressions | ||
let type = this.getTypeOfExpression(node); | ||
@@ -207,3 +209,6 @@ type = this.checker.getBaseConstraintOfType(type) || type; | ||
return; | ||
const result = this.executePredicate(type, predicate.nullable ? predicate.check : (t) => tsutils_1.isEmptyObjectType(t) ? undefined : predicate.check(t)); | ||
const result = this.executePredicate(type, | ||
// empty object type can contain anything but `null | undefined` | ||
// TODO use assignability check to avoid false positives | ||
predicate.nullable ? predicate.check : (t) => tsutils_1.isEmptyObjectType(t) ? undefined : predicate.check(t)); | ||
return result && !this.strictNullChecks | ||
@@ -241,2 +246,7 @@ ? undefined | ||
} | ||
/** | ||
* If one of the types in the intersection matches the precicate, this function returns true. | ||
* Otherwise if any of the types results in undefined, this function returns undefined. | ||
* If every type results in false, it returns false. | ||
*/ | ||
matchIntersectionType(type, predicate) { | ||
@@ -279,3 +289,3 @@ let result = false; | ||
return; | ||
const names = utils_1.lateBoundPropertyNames(name, this.checker); | ||
const names = utils_1.lateBoundPropertyNames(name, this.checker); // TODO lateBoundPropertyNames should also be aware of index signatures | ||
if (!names.known) | ||
@@ -285,6 +295,7 @@ return; | ||
for (const { symbolName } of names.properties) { | ||
// we check every union type member separately because symbols lose optionality when accessed through union types | ||
for (const type of types) { | ||
const symbol = utils_1.getPropertyOfType(type, symbolName); | ||
if (symbol === undefined || symbol.flags & ts.SymbolFlags.Optional) | ||
return; | ||
return; // it might be present at runtime, so we don't return false | ||
} | ||
@@ -311,2 +322,3 @@ } | ||
} | ||
// TODO use assignability check | ||
return tsutils_1.isEmptyObjectType(type) ? undefined : true; | ||
@@ -337,2 +349,3 @@ } | ||
return true; | ||
// check if this could be the global `Function` type | ||
return type.symbol !== undefined && type.symbol.escapedName === 'Function' && | ||
@@ -339,0 +352,0 @@ hasPropertyOfKind(type, 'apply', ts.SymbolFlags.Method) && |
@@ -61,2 +61,4 @@ "use strict"; | ||
default: | ||
// TODO do something special for MethodDeclaration? | ||
// exclude accessors | ||
return false; | ||
@@ -71,3 +73,3 @@ } | ||
for (const property of properties) { | ||
fix.push(ymir_1.Replacement.replace(prevEnd, property.pos, ' ')); | ||
fix.push(ymir_1.Replacement.replace(prevEnd, property.pos, ' ')); // use space instead of comma as separator | ||
switch (property.kind) { | ||
@@ -90,2 +92,3 @@ case ts.SyntaxKind.SpreadAssignment: | ||
if (elements.length === 0) { | ||
// handle special case of empty object or array to remove trailing comma | ||
const parent = node.parent; | ||
@@ -92,0 +95,0 @@ const containingList = parent.kind === ts.SyntaxKind.ArrayLiteralExpression |
@@ -12,5 +12,5 @@ "use strict"; | ||
this.strictFile = this.context.compilerOptions !== undefined && tsutils_1.isStrictCompilerOptionEnabled(this.context.compilerOptions, 'alwaysStrict') | ||
? "due to the compilerOption 'alwaysStrict' this code is in strict mode" | ||
? "due to the compilerOption 'alwaysStrict' this code is in strict mode" /* Option */ | ||
: ts.isExternalModule(this.sourceFile) | ||
? "ES6 modules are always in strict mode" | ||
? "ES6 modules are always in strict mode" /* Module */ | ||
: undefined; | ||
@@ -33,3 +33,3 @@ } | ||
if (!utils_1.hasDirectivePrologue(parent)) | ||
return; | ||
return; // not a directive | ||
let reason = this.strictFile; | ||
@@ -40,3 +40,3 @@ for (const statement of parent.statements) { | ||
reason = this.isInStrictContext(parent.parent.parent) || | ||
(this.hasUseStrictDirective(this.sourceFile) ? "a parent node is already in strict mode" : undefined); | ||
(this.hasUseStrictDirective(this.sourceFile) ? "a parent node is already in strict mode" /* Parent */ : undefined); | ||
if (reason !== undefined) | ||
@@ -47,5 +47,5 @@ this.addFindingAtNode(directive, `Redundant 'use strict': ${reason}.`, ymir_1.Replacement.delete(directive.pos, directive.end)); | ||
if (!tsutils_1.isExpressionStatement(statement) || !tsutils_1.isStringLiteral(statement.expression)) | ||
return; | ||
return; // not a directive | ||
if (reason === undefined && statement.expression.getText(this.sourceFile).slice(1, -1) === 'use strict') | ||
reason = "there is already a 'use strict' directive in this prologue"; | ||
reason = "there is already a 'use strict' directive in this prologue" /* Antedecent */; | ||
} | ||
@@ -58,9 +58,9 @@ } | ||
case ts.SyntaxKind.ClassExpression: | ||
return "classes are always in strict mode"; | ||
return "classes are always in strict mode" /* Class */; | ||
case ts.SyntaxKind.SourceFile: | ||
return; | ||
return; // SourceFile was already checked | ||
} | ||
if (utils_1.hasDirectivePrologue(node)) { | ||
if (this.hasUseStrictDirective(node)) | ||
return "a parent node is already in strict mode"; | ||
return "a parent node is already in strict mode" /* Parent */; | ||
node = node.parent.parent; | ||
@@ -67,0 +67,0 @@ } |
@@ -18,2 +18,3 @@ "use strict"; | ||
else if (catchClause !== undefined && isRethrow(catchClause)) { | ||
// reminder for myself: empty catch clause can be used to simply ignore errors and is never redundant | ||
this.addFinding(start, node.end, `${hasFinally ? "'catch' clause" : "'try' statement"} is unnecessary because the 'catch' clause only rethrows the error.`, hasFinally | ||
@@ -20,0 +21,0 @@ ? ymir_1.Replacement.delete(catchClause.getStart(this.sourceFile), finallyBlock.pos - 'finally'.length) |
@@ -91,4 +91,7 @@ "use strict"; | ||
return [ | ||
/* Add assignment after directives and super() call (if they exists) */ | ||
ymir_1.Replacement.append(getEndOfDirectivesAndSuper(construct), `${lineBreak}this.${param.name.getText()} = ${param.name.getText()};`), | ||
/* Add property to class */ | ||
ymir_1.Replacement.append(construct.getStart(), getPropertyTextFromParam(param) + lineBreak), | ||
/* Finally, delete modifiers from the parameter prop */ | ||
ymir_1.Replacement.delete(param.modifiers.pos, param.modifiers.end), | ||
@@ -98,2 +101,3 @@ ]; | ||
function getFixerForLonghandProp(param, construct) { | ||
/* Class member and assignment to be removed */ | ||
const member = construct | ||
@@ -108,2 +112,3 @@ .parent.members.filter(tsutils_1.isPropertyDeclaration) | ||
ymir_1.Replacement.delete(assignment.pos, assignment.end), | ||
/* Prepend access modifiers to parameter declaration */ | ||
ymir_1.Replacement.append(param.name.getStart(), member.modifiers ? member.modifiers.map((mod) => mod.getText()).join(' ') + ' ' : 'public '), | ||
@@ -118,2 +123,6 @@ ]; | ||
} | ||
/** | ||
* If directive(s) such as 'use strict' exist in the constructor body, returns | ||
* the last directive, otherwise undefined. | ||
*/ | ||
function getFinalDirectiveIndex(construct) { | ||
@@ -130,2 +139,5 @@ let finalDirectiveIndex = -1; | ||
} | ||
/** | ||
* Takes a parameter property and returns the text of the equivalent class element. | ||
*/ | ||
function getPropertyTextFromParam(param) { | ||
@@ -141,2 +153,5 @@ const modifiers = param.modifiers ? param.modifiers.map((mod) => mod.getText()).join(' ') : ''; | ||
} | ||
/** | ||
* Returns the super call expression index if it exists, otherwise -1. | ||
*/ | ||
function getSuperCallIndex(construct) { | ||
@@ -149,2 +164,5 @@ const statementIndex = getFinalDirectiveIndex(construct) + 1; | ||
} | ||
/** | ||
* Returns true when the prop is assigned any value other than the param identifier. | ||
*/ | ||
function isNonParamAssignmentToProp(stmt, param) { | ||
@@ -165,2 +183,5 @@ return (tsutils_1.isBinaryExpression(stmt.expression) && | ||
} | ||
/** | ||
* Checks if a statement assigns the parameter to a class member property. | ||
*/ | ||
function isSimpleParamToPropAssignment(stmt, param) { | ||
@@ -180,2 +201,6 @@ return (tsutils_1.isExpressionStatement(stmt) && | ||
} | ||
/** | ||
* We can only autofix longhand props -> param props if certain conditions are met within the constructor body. | ||
* See https://github.com/fimbullinter/wotan/issues/167#issuecomment-378945862 for more details on these conditions. | ||
*/ | ||
function isStatementWithPossibleSideEffects(stmt, param) { | ||
@@ -189,2 +214,6 @@ return (!tsutils_1.isExpressionStatement(stmt) || | ||
} | ||
/** | ||
* Returns all statements in a constructor body with the exception of super calls | ||
* and directives such as 'use strict'. | ||
*/ | ||
function statementsMinusDirectivesAndSuper(construct) { | ||
@@ -191,0 +220,0 @@ return construct.body.statements.slice(Math.max(getFinalDirectiveIndex(construct), getSuperCallIndex(construct)) + 1); |
@@ -124,6 +124,6 @@ "use strict"; | ||
if (getFunctionScopeBoundary(useScope) !== functionScope) | ||
return true; | ||
return true; // maybe used before declaration | ||
for (const deadZone of deadZones) | ||
if (use.location.pos >= deadZone.pos && use.location.pos < deadZone.end) | ||
return true; | ||
return true; // used in temporal dead zone | ||
} | ||
@@ -130,0 +130,0 @@ } |
@@ -52,3 +52,3 @@ "use strict"; | ||
if (type.flags & ts.TypeFlags.StringLike) | ||
return this.sourceFile.languageVersion >= ts.ScriptTarget.ES5; | ||
return this.sourceFile.languageVersion >= ts.ScriptTarget.ES5; // iterating string is only possible starting from ES5 | ||
if (type.symbol !== undefined && /^(Concat|Readonly)?Array$/.test(type.symbol.escapedName) && | ||
@@ -63,2 +63,3 @@ type.symbol.declarations !== undefined && type.symbol.declarations.some(this.isDeclaredInDefaultLib, this)) | ||
isDeclaredInDefaultLib(node) { | ||
// we assume it's the global array type if it comes from any lib.xxx.d.ts file | ||
return path.normalize(path.dirname(node.getSourceFile().fileName)) | ||
@@ -132,5 +133,5 @@ === path.dirname(ts.getDefaultLibFilePath(this.context.compilerOptions)); | ||
if (use.pos < statement.pos || use.end > statement.end) | ||
return false; | ||
return false; // used outside of the loop | ||
if (use.pos < statement.statement.pos) | ||
continue; | ||
continue; // uses in loop header are already checked | ||
const parent = use.parent; | ||
@@ -144,3 +145,3 @@ if (!tsutils_1.isElementAccessExpression(parent) || | ||
} | ||
return arrayAccess; | ||
return arrayAccess; // needs at least one array access | ||
} | ||
@@ -154,5 +155,7 @@ function extractArrayVariable(condition, indexVariable) { | ||
case ts.SyntaxKind.LessThanToken: | ||
// i < foo.length | ||
({ left, right } = condition); | ||
break; | ||
case ts.SyntaxKind.GreaterThanToken: | ||
// foo.length > i | ||
({ left: right, right: left } = condition); | ||
@@ -168,2 +171,3 @@ break; | ||
function extractIndexVariable(initializer, incrementor) { | ||
// there must be only one variable declared | ||
if (!tsutils_1.isVariableDeclarationList(initializer) || | ||
@@ -173,5 +177,7 @@ initializer.declarations.length !== 1) | ||
const declaration = initializer.declarations[0]; | ||
// variable must be initialized to 0 | ||
if (declaration.name.kind !== ts.SyntaxKind.Identifier || declaration.initializer === undefined || | ||
!isNumber(declaration.initializer, 0)) | ||
return; | ||
// variable must be incremented by one | ||
if (!isIncrementedByOne(incrementor, declaration.name.text)) | ||
@@ -183,2 +189,3 @@ return; | ||
if (tsutils_1.isPostfixUnaryExpression(node) || tsutils_1.isPrefixUnaryExpression(node)) | ||
// ++var or var++ | ||
return node.operator === ts.SyntaxKind.PlusPlusToken && isVariable(node.operand, name); | ||
@@ -189,4 +196,6 @@ if (!tsutils_1.isBinaryExpression(node)) | ||
case ts.SyntaxKind.PlusEqualsToken: | ||
// var += 1 | ||
return isVariable(node.left, name) && isNumber(node.right, 1); | ||
case ts.SyntaxKind.EqualsToken: | ||
// var = var + 1 or var = 1 + var | ||
return isVariable(node.left, name) && | ||
@@ -193,0 +202,0 @@ tsutils_1.isBinaryExpression(node.right) && node.right.operatorToken.kind === ts.SyntaxKind.PlusToken && |
@@ -82,2 +82,3 @@ "use strict"; | ||
} | ||
// remove empty object iteral and the following comma if exists | ||
fix.push(ymir_1.Replacement.delete(removedPrevious ? arg.getStart(sourceFile) : arg.pos, end)); | ||
@@ -88,3 +89,7 @@ removedPrevious = true; | ||
const start = arg.getStart(sourceFile); | ||
fix.push(ymir_1.Replacement.delete(start, start + 1), ymir_1.Replacement.delete(arg.properties[arg.properties.length - 1].end, arg.end)); | ||
fix.push( | ||
// remove open brace | ||
ymir_1.Replacement.delete(start, start + 1), | ||
// remove trailing comma if exists and close brace | ||
ymir_1.Replacement.delete(arg.properties[arg.properties.length - 1].end, arg.end)); | ||
removedPrevious = false; | ||
@@ -91,0 +96,0 @@ } |
@@ -18,2 +18,3 @@ "use strict"; | ||
node.tryBlock.statements.forEach(this.visitStatement, this); | ||
// special handling for catchClause only if finallyBlock is present | ||
if (node.catchClause !== undefined && node.finallyBlock !== undefined) | ||
@@ -20,0 +21,0 @@ node.catchClause.block.statements.forEach(this.visitStatement, this); |
@@ -59,2 +59,3 @@ "use strict"; | ||
let parent = node.parent; | ||
// fixing unary expression like 'await <T>foo' to 'await foo as T' asserts the result of the entire expression, not just the operand | ||
switch (parent.kind) { | ||
@@ -67,2 +68,3 @@ case ts.SyntaxKind.PrefixUnaryExpression: | ||
} | ||
// fixing '<T>foo & bar' to 'foo as T & bar' would parse 'T & bar' as intersection type, therefore we need to add parens | ||
while (tsutils_1.isBinaryExpression(parent)) { | ||
@@ -69,0 +71,0 @@ if (node === parent.left) { |
@@ -33,5 +33,7 @@ "use strict"; | ||
return false; | ||
// falls through | ||
case ts.SyntaxKind.ArrowFunction: | ||
if (node.body.kind !== ts.SyntaxKind.Block) | ||
return false; | ||
// falls through | ||
case ts.SyntaxKind.FunctionExpression: | ||
@@ -102,2 +104,8 @@ break; | ||
function expressionNeedsParensWhenReplacingNode(expr, replaced) { | ||
// this currently doesn't handle the following cases | ||
// (yield) as any | ||
// await (yield) | ||
// (1).toString() | ||
// ({foo} = {foo: 1}); | ||
// binary operator precendence | ||
while (true) { | ||
@@ -104,0 +112,0 @@ switch (expr.kind) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
417896
4917
+ Added@fimbul/ymir@0.21.0-dev.20190329(transitive)
- Removed@fimbul/ymir@0.21.0-dev.20190322(transitive)