Skip to content

Commit

Permalink
Merge branch 'main' into v8
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Apr 25, 2024
2 parents f4cbbe6 + 3fd666f commit 8284a18
Show file tree
Hide file tree
Showing 22 changed files with 590 additions and 121 deletions.
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"Airbnb",
"Airbnb's",
"ambiently",
"allowdefaultprojectforfiles",
"Armano",
"astexplorer",
"Astro",
Expand Down Expand Up @@ -150,6 +151,7 @@
"unoptimized",
"unprefixed",
"upsert",
"useprojectservice",
"Waiblinger",
"warnonunsupportedtypescriptversion",
"Zacher"
Expand Down
9 changes: 9 additions & 0 deletions docs/packages/TypeScript_ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,15 @@ interface ProjectServiceOptions {
* Path to a TSConfig to use instead of TypeScript's default project configuration.
*/
defaultProject?: string;

/**
* The maximum number of files {@link allowDefaultProjectForFiles} may match.
* Each file match slows down linting, so if you do need to use this, please
* file an informative issue on typescript-eslint explaining why - so we can
* help you avoid using it!
* @default 8
*/
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING?: number;
}

interface ParserServices {
Expand Down
36 changes: 35 additions & 1 deletion docs/troubleshooting/FAQ.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,40 @@ If you don't find an existing extension rule, or the extension rule doesn't work
> We release a new version our tooling every week.
> _Please_ ensure that you [check our the latest list of "extension" rules](/rules/#extension-rules) **_before_** filing an issue.
<HiddenHeading id="allowdefaultprojectforfiles-glob-too-wide" />

## I get errors telling me "Having many files run with the default project is known to cause performance issues and slow down linting."

These errors are caused by using the [`EXPERIMENTAL_useProjectService`](../packages/Parser.mdx#experimental_useprojectservice) `allowDefaultProjectForFiles` with an excessively wide glob.
`allowDefaultProjectForFiles` causes a new TypeScript "program" to be built for each "out of project" file it includes, which incurs a performance overhead for each file.

To resolve this error, narrow the glob(s) used for `allowDefaultProjectForFiles` to include fewer files.
For example:

```diff title="eslint.config.js"
parserOptions: {
EXPERIMENTAL_useProjectService: {
allowDefaultProjectForFiles: [
- "**/*.js",
+ "./*.js"
]
}
}
```

You may also need to include more files in your `tsconfig.json`.
For example:

```diff title="tsconfig.json"
"include": [
"src",
+ "*.js"
]
```

If you cannot do this, please [file an issue on typescript-eslint's typescript-estree package](https://github.com/typescript-eslint/typescript-eslint/issues/new?assignees=&labels=enhancement%2Ctriage&projects=&template=07-enhancement-other.yaml&title=Enhancement%3A+%3Ca+short+description+of+my+proposal%3E) telling us your use case and why you need more out-of-project files linted.
Be sure to include a minimal reproduction we can work with to understand your use case!

## I get errors telling me "ESLint was configured to run ... However, that TSConfig does not / none of those TSConfigs include this file"

These errors are caused by an ESLint config requesting type information be generated for a file that isn't included in the TypeScript configuration.
Expand Down Expand Up @@ -499,6 +533,6 @@ If you think you're having issues with performance, see our [Performance Trouble

## Are TypeScript project references supported?

No, TypeScript project references are not yet supported.
Yes, but only with [`EXPERIMENTAL_useProjectService`](../packages/Parser.mdx#experimental_useprojectservice).

See [issue #2094 discussing project references](https://github.com/typescript-eslint/typescript-eslint/issues/2094) for more details.
206 changes: 112 additions & 94 deletions packages/eslint-plugin/src/rules/no-unsafe-argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isTypeAnyArrayType,
isTypeAnyType,
isUnsafeAssignment,
nullThrows,
} from '../util';

type MessageIds =
Expand Down Expand Up @@ -162,114 +163,131 @@ export default createRule<[], MessageIds>({
const services = getParserServices(context);
const checker = services.program.getTypeChecker();

return {
'CallExpression, NewExpression'(
node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
if (node.arguments.length === 0) {
return;
}
function checkUnsafeArguments(
args: TSESTree.Expression[] | TSESTree.CallExpressionArgument[],
callee: TSESTree.LeftHandSideExpression,
node:
| TSESTree.CallExpression
| TSESTree.NewExpression
| TSESTree.TaggedTemplateExpression,
): void {
if (args.length === 0) {
return;
}

// ignore any-typed calls as these are caught by no-unsafe-call
if (isTypeAnyType(services.getTypeAtLocation(node.callee))) {
return;
}
// ignore any-typed calls as these are caught by no-unsafe-call
if (isTypeAnyType(services.getTypeAtLocation(callee))) {
return;
}

const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const signature = FunctionSignature.create(checker, tsNode);
if (!signature) {
return;
}
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const signature = nullThrows(
FunctionSignature.create(checker, tsNode),
'Expected to a signature resolved',
);

for (const argument of node.arguments) {
switch (argument.type) {
// spreads consume
case AST_NODE_TYPES.SpreadElement: {
const spreadArgType = services.getTypeAtLocation(
argument.argument,
);
if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) {
// Consumes the first parameter (TemplateStringsArray) of the function called with TaggedTemplateExpression.
signature.getNextParameterType();
}

for (const argument of args) {
switch (argument.type) {
// spreads consume
case AST_NODE_TYPES.SpreadElement: {
const spreadArgType = services.getTypeAtLocation(argument.argument);

if (isTypeAnyType(spreadArgType)) {
// foo(...any)
context.report({
node: argument,
messageId: 'unsafeSpread',
});
} else if (isTypeAnyArrayType(spreadArgType, checker)) {
// foo(...any[])
if (isTypeAnyType(spreadArgType)) {
// foo(...any)
context.report({
node: argument,
messageId: 'unsafeSpread',
});
} else if (isTypeAnyArrayType(spreadArgType, checker)) {
// foo(...any[])

// TODO - we could break down the spread and compare the array type against each argument
context.report({
node: argument,
messageId: 'unsafeArraySpread',
});
} else if (checker.isTupleType(spreadArgType)) {
// foo(...[tuple1, tuple2])
const spreadTypeArguments =
checker.getTypeArguments(spreadArgType);
for (const tupleType of spreadTypeArguments) {
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}
const result = isUnsafeAssignment(
tupleType,
parameterType,
checker,
// we can't pass the individual tuple members in here as this will most likely be a spread variable
// not a spread array
null,
);
if (result) {
context.report({
node: argument,
messageId: 'unsafeTupleSpread',
data: {
sender: checker.typeToString(tupleType),
receiver: checker.typeToString(parameterType),
},
});
}
// TODO - we could break down the spread and compare the array type against each argument
context.report({
node: argument,
messageId: 'unsafeArraySpread',
});
} else if (checker.isTupleType(spreadArgType)) {
// foo(...[tuple1, tuple2])
const spreadTypeArguments =
checker.getTypeArguments(spreadArgType);
for (const tupleType of spreadTypeArguments) {
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}
if (spreadArgType.target.hasRestElement) {
// the last element was a rest - so all remaining defined arguments can be considered "consumed"
// all remaining arguments should be compared against the rest type (if one exists)
signature.consumeRemainingArguments();
const result = isUnsafeAssignment(
tupleType,
parameterType,
checker,
// we can't pass the individual tuple members in here as this will most likely be a spread variable
// not a spread array
null,
);
if (result) {
context.report({
node: argument,
messageId: 'unsafeTupleSpread',
data: {
sender: checker.typeToString(tupleType),
receiver: checker.typeToString(parameterType),
},
});
}
} else {
// something that's iterable
// handling this will be pretty complex - so we ignore it for now
// TODO - handle generic iterable case
}
break;
if (spreadArgType.target.hasRestElement) {
// the last element was a rest - so all remaining defined arguments can be considered "consumed"
// all remaining arguments should be compared against the rest type (if one exists)
signature.consumeRemainingArguments();
}
} else {
// something that's iterable
// handling this will be pretty complex - so we ignore it for now
// TODO - handle generic iterable case
}
break;
}

default: {
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}
default: {
const parameterType = signature.getNextParameterType();
if (parameterType == null) {
continue;
}

const argumentType = services.getTypeAtLocation(argument);
const result = isUnsafeAssignment(
argumentType,
parameterType,
checker,
argument,
);
if (result) {
context.report({
node: argument,
messageId: 'unsafeArgument',
data: {
sender: checker.typeToString(argumentType),
receiver: checker.typeToString(parameterType),
},
});
}
const argumentType = services.getTypeAtLocation(argument);
const result = isUnsafeAssignment(
argumentType,
parameterType,
checker,
argument,
);
if (result) {
context.report({
node: argument,
messageId: 'unsafeArgument',
data: {
sender: checker.typeToString(argumentType),
receiver: checker.typeToString(parameterType),
},
});
}
}
}
}
}

return {
'CallExpression, NewExpression'(
node: TSESTree.CallExpression | TSESTree.NewExpression,
): void {
checkUnsafeArguments(node.arguments, node.callee, node);
},
TaggedTemplateExpression(node: TSESTree.TaggedTemplateExpression): void {
checkUnsafeArguments(node.quasi.expressions, node.tag, node);
},
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,24 @@ const analyzeAndChainOperand: OperandAnalyzer = (
chain,
) => {
switch (operand.comparisonType) {
case NullishComparisonType.Boolean:
case NullishComparisonType.Boolean: {
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualNull &&
operand.comparedName.type === AST_NODE_TYPES.Identifier
) {
return null;
}
return [operand];
}

case NullishComparisonType.NotEqualNullOrUndefined:
return [operand];

case NullishComparisonType.NotStrictEqualNull: {
// handle `x !== null && x !== undefined`
const nextOperand = chain[index + 1] as ValidOperand | undefined;
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualUndefined &&
Expand All @@ -94,7 +105,7 @@ const analyzeAndChainOperand: OperandAnalyzer = (

case NullishComparisonType.NotStrictEqualUndefined: {
// handle `x !== undefined && x !== null`
const nextOperand = chain[index + 1] as ValidOperand | undefined;
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.NotStrictEqualNull &&
Expand Down Expand Up @@ -132,7 +143,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (

case NullishComparisonType.StrictEqualNull: {
// handle `x === null || x === undefined`
const nextOperand = chain[index + 1] as ValidOperand | undefined;
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType ===
NullishComparisonType.StrictEqualUndefined &&
Expand All @@ -159,7 +170,7 @@ const analyzeOrChainOperand: OperandAnalyzer = (

case NullishComparisonType.StrictEqualUndefined: {
// handle `x === undefined || x === null`
const nextOperand = chain[index + 1] as ValidOperand | undefined;
const nextOperand = chain.at(index + 1);
if (
nextOperand?.comparisonType === NullishComparisonType.StrictEqualNull &&
compareNodes(operand.comparedName, nextOperand.comparedName) ===
Expand Down

0 comments on commit 8284a18

Please sign in to comment.