Skip to content

Commit 9634b83

Browse files
Merge branch 'main' into fix/website-error
2 parents fdc1212 + f74a8fa commit 9634b83

File tree

32 files changed

+702
-120
lines changed

32 files changed

+702
-120
lines changed

Diff for: CHANGELOG.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
## 8.16.0 (2024-11-25)
2+
3+
### 🚀 Features
4+
5+
- support TypeScript 5.7 ([#10372](https://github.com/typescript-eslint/typescript-eslint/pull/10372))
6+
- **eslint-plugin:** [max-params] add function overload and function type support ([#10312](https://github.com/typescript-eslint/typescript-eslint/pull/10312))
7+
- **eslint-plugin:** [no-base-to-string] check Array.prototype.join ([#10287](https://github.com/typescript-eslint/typescript-eslint/pull/10287))
8+
9+
### 🩹 Fixes
10+
11+
- **typescript-estree:** update conditions for unsupported version warning ([#10385](https://github.com/typescript-eslint/typescript-eslint/pull/10385))
12+
13+
### ❤️ Thank You
14+
15+
- Inga @inga-lovinde
16+
- Josh Goldberg ✨
17+
- Kim Sang Du @developer-bandi
18+
- YeonJuan @yeonjuan
19+
20+
You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.
21+
122
## 8.15.0 (2024-11-18)
223

324
### 🚀 Features

Diff for: packages/ast-spec/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 8.16.0 (2024-11-25)
2+
3+
This was a version bump only for ast-spec to align it with other projects, there were no code changes.
4+
5+
You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.
6+
17
## 8.15.0 (2024-11-18)
28

39
This was a version bump only for ast-spec to align it with other projects, there were no code changes.

Diff for: packages/ast-spec/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@typescript-eslint/ast-spec",
3-
"version": "8.15.0",
3+
"version": "8.16.0",
44
"description": "Complete specification for the TypeScript-ESTree AST",
55
"private": true,
66
"keywords": [

Diff for: packages/eslint-plugin/CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## 8.16.0 (2024-11-25)
2+
3+
### 🚀 Features
4+
5+
- **eslint-plugin:** [no-base-to-string] check Array.prototype.join ([#10287](https://github.com/typescript-eslint/typescript-eslint/pull/10287))
6+
- **eslint-plugin:** [max-params] add function overload and function type support ([#10312](https://github.com/typescript-eslint/typescript-eslint/pull/10312))
7+
8+
### ❤️ Thank You
9+
10+
- Kim Sang Du @developer-bandi
11+
- YeonJuan @yeonjuan
12+
13+
You can read about our [versioning strategy](https://main--typescript-eslint.netlify.app/users/versioning) and [releases](https://main--typescript-eslint.netlify.app/users/releases) on our website.
14+
115
## 8.15.0 (2024-11-18)
216

317
### 🚀 Features

Diff for: packages/eslint-plugin/docs/rules/no-base-to-string.mdx

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ value + '';
3434
String({});
3535
({}).toString();
3636
({}).toLocaleString();
37+
38+
// Stringifying objects or instances in an array with the `Array.prototype.join`.
39+
[{}, new MyClass()].join('');
3740
```
3841

3942
</TabItem>

Diff for: packages/eslint-plugin/package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@typescript-eslint/eslint-plugin",
3-
"version": "8.15.0",
3+
"version": "8.16.0",
44
"description": "TypeScript plugin for ESLint",
55
"files": [
66
"dist",
@@ -61,10 +61,10 @@
6161
},
6262
"dependencies": {
6363
"@eslint-community/regexpp": "^4.10.0",
64-
"@typescript-eslint/scope-manager": "8.15.0",
65-
"@typescript-eslint/type-utils": "8.15.0",
66-
"@typescript-eslint/utils": "8.15.0",
67-
"@typescript-eslint/visitor-keys": "8.15.0",
64+
"@typescript-eslint/scope-manager": "8.16.0",
65+
"@typescript-eslint/type-utils": "8.16.0",
66+
"@typescript-eslint/utils": "8.16.0",
67+
"@typescript-eslint/visitor-keys": "8.16.0",
6868
"graphemer": "^1.4.0",
6969
"ignore": "^5.3.1",
7070
"natural-compare": "^1.4.0",
@@ -75,8 +75,8 @@
7575
"@types/marked": "^5.0.2",
7676
"@types/mdast": "^4.0.3",
7777
"@types/natural-compare": "*",
78-
"@typescript-eslint/rule-schema-to-typescript-types": "8.15.0",
79-
"@typescript-eslint/rule-tester": "8.15.0",
78+
"@typescript-eslint/rule-schema-to-typescript-types": "8.16.0",
79+
"@typescript-eslint/rule-tester": "8.16.0",
8080
"ajv": "^6.12.6",
8181
"cross-env": "^7.0.3",
8282
"cross-fetch": "*",

Diff for: packages/eslint-plugin/src/rules/no-base-to-string.ts

+106-37
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
import type { TSESTree } from '@typescript-eslint/utils';
33

44
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
5+
import * as tsutils from 'ts-api-utils';
56
import * as ts from 'typescript';
67

7-
import { createRule, getParserServices, getTypeName } from '../util';
8+
import {
9+
createRule,
10+
getConstrainedTypeAtLocation,
11+
getParserServices,
12+
getTypeName,
13+
nullThrows,
14+
} from '../util';
815

916
enum Usefulness {
1017
Always = 'always',
@@ -17,7 +24,7 @@ type Options = [
1724
ignoredTypeNames?: string[];
1825
},
1926
];
20-
type MessageIds = 'baseToString';
27+
type MessageIds = 'baseArrayJoin' | 'baseToString';
2128

2229
export default createRule<Options, MessageIds>({
2330
name: 'no-base-to-string',
@@ -30,6 +37,8 @@ export default createRule<Options, MessageIds>({
3037
requiresTypeChecking: true,
3138
},
3239
messages: {
40+
baseArrayJoin:
41+
"Using `join()` for {{name}} {{certainty}} use Object's default stringification format ('[object Object]') when stringified.",
3342
baseToString:
3443
"'{{name}}' {{certainty}} use Object's default stringification format ('[object Object]') when stringified.",
3544
},
@@ -64,7 +73,6 @@ export default createRule<Options, MessageIds>({
6473
if (node.type === AST_NODE_TYPES.Literal) {
6574
return;
6675
}
67-
6876
const certainty = collectToStringCertainty(
6977
type ?? services.getTypeAtLocation(node),
7078
);
@@ -82,6 +90,91 @@ export default createRule<Options, MessageIds>({
8290
});
8391
}
8492

93+
function checkExpressionForArrayJoin(
94+
node: TSESTree.Node,
95+
type: ts.Type,
96+
): void {
97+
const certainty = collectJoinCertainty(type);
98+
99+
if (certainty === Usefulness.Always) {
100+
return;
101+
}
102+
103+
context.report({
104+
node,
105+
messageId: 'baseArrayJoin',
106+
data: {
107+
name: context.sourceCode.getText(node),
108+
certainty,
109+
},
110+
});
111+
}
112+
113+
function collectUnionTypeCertainty(
114+
type: ts.UnionType,
115+
collectSubTypeCertainty: (type: ts.Type) => Usefulness,
116+
): Usefulness {
117+
const certainties = type.types.map(t => collectSubTypeCertainty(t));
118+
if (certainties.every(certainty => certainty === Usefulness.Never)) {
119+
return Usefulness.Never;
120+
}
121+
122+
if (certainties.every(certainty => certainty === Usefulness.Always)) {
123+
return Usefulness.Always;
124+
}
125+
126+
return Usefulness.Sometimes;
127+
}
128+
129+
function collectIntersectionTypeCertainty(
130+
type: ts.IntersectionType,
131+
collectSubTypeCertainty: (type: ts.Type) => Usefulness,
132+
): Usefulness {
133+
for (const subType of type.types) {
134+
const subtypeUsefulness = collectSubTypeCertainty(subType);
135+
136+
if (subtypeUsefulness === Usefulness.Always) {
137+
return Usefulness.Always;
138+
}
139+
}
140+
141+
return Usefulness.Never;
142+
}
143+
144+
function collectJoinCertainty(type: ts.Type): Usefulness {
145+
if (tsutils.isUnionType(type)) {
146+
return collectUnionTypeCertainty(type, collectJoinCertainty);
147+
}
148+
149+
if (tsutils.isIntersectionType(type)) {
150+
return collectIntersectionTypeCertainty(type, collectJoinCertainty);
151+
}
152+
153+
if (checker.isTupleType(type)) {
154+
const typeArgs = checker.getTypeArguments(type);
155+
const certainties = typeArgs.map(t => collectToStringCertainty(t));
156+
if (certainties.some(certainty => certainty === Usefulness.Never)) {
157+
return Usefulness.Never;
158+
}
159+
160+
if (certainties.some(certainty => certainty === Usefulness.Sometimes)) {
161+
return Usefulness.Sometimes;
162+
}
163+
164+
return Usefulness.Always;
165+
}
166+
167+
if (checker.isArrayType(type)) {
168+
const elemType = nullThrows(
169+
type.getNumberIndexType(),
170+
'array should have number index type',
171+
);
172+
return collectToStringCertainty(elemType);
173+
}
174+
175+
return Usefulness.Always;
176+
}
177+
85178
function collectToStringCertainty(type: ts.Type): Usefulness {
86179
const toString =
87180
checker.getPropertyOfType(type, 'toString') ??
@@ -113,45 +206,13 @@ export default createRule<Options, MessageIds>({
113206
}
114207

115208
if (type.isIntersection()) {
116-
for (const subType of type.types) {
117-
const subtypeUsefulness = collectToStringCertainty(subType);
118-
119-
if (subtypeUsefulness === Usefulness.Always) {
120-
return Usefulness.Always;
121-
}
122-
}
123-
124-
return Usefulness.Never;
209+
return collectIntersectionTypeCertainty(type, collectToStringCertainty);
125210
}
126211

127212
if (!type.isUnion()) {
128213
return Usefulness.Never;
129214
}
130-
131-
let allSubtypesUseful = true;
132-
let someSubtypeUseful = false;
133-
134-
for (const subType of type.types) {
135-
const subtypeUsefulness = collectToStringCertainty(subType);
136-
137-
if (subtypeUsefulness !== Usefulness.Always && allSubtypesUseful) {
138-
allSubtypesUseful = false;
139-
}
140-
141-
if (subtypeUsefulness !== Usefulness.Never && !someSubtypeUseful) {
142-
someSubtypeUseful = true;
143-
}
144-
}
145-
146-
if (allSubtypesUseful && someSubtypeUseful) {
147-
return Usefulness.Always;
148-
}
149-
150-
if (someSubtypeUseful) {
151-
return Usefulness.Sometimes;
152-
}
153-
154-
return Usefulness.Never;
215+
return collectUnionTypeCertainty(type, collectToStringCertainty);
155216
}
156217

157218
function isBuiltInStringCall(node: TSESTree.CallExpression): boolean {
@@ -188,12 +249,20 @@ export default createRule<Options, MessageIds>({
188249
checkExpression(node.arguments[0]);
189250
}
190251
},
252+
'CallExpression > MemberExpression.callee > Identifier[name = "join"].property'(
253+
node: TSESTree.Expression,
254+
): void {
255+
const memberExpr = node.parent as TSESTree.MemberExpression;
256+
const type = getConstrainedTypeAtLocation(services, memberExpr.object);
257+
checkExpressionForArrayJoin(memberExpr.object, type);
258+
},
191259
'CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property'(
192260
node: TSESTree.Expression,
193261
): void {
194262
const memberExpr = node.parent as TSESTree.MemberExpression;
195263
checkExpression(memberExpr.object);
196264
},
265+
197266
TemplateLiteral(node: TSESTree.TemplateLiteral): void {
198267
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
199268
return;

Diff for: packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)