Skip to content

Commit e76d467

Browse files
francois-mora-sonarsourceerwan-leforestier-sonarsourceclaude
authored
JS-1731 Fix S6551 typeof guard false positives (#7404)
Co-authored-by: Erwan Le Forestier <erwan.leforestier@sonarsource.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4ae9016 commit e76d467

8 files changed

Lines changed: 1100 additions & 416 deletions

File tree

its/ruling/src/test/expected/vitest/typescript-S6551.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
"vitest:packages/utils/src/diff/getType.ts": [
1515
65
1616
],
17-
"vitest:packages/vitest/src/node/cli/cli-config.ts": [
18-
785,
19-
795
20-
],
2117
"vitest:packages/vitest/src/node/plugins/index.ts": [
2218
258
2319
],

packages/analysis/src/jsts/rules/S6551/cb.fixture.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,132 @@ function laterUnguardedResultUseStillUnsafe(data: object, fallback: string) {
152152
}
153153
return rendered || fallback;
154154
}
155+
156+
function guardedByTypeofNotObject(value: unknown): string {
157+
if (typeof value !== 'object') {
158+
return `Unexpected value: ${value}`; // Compliant
159+
}
160+
return '';
161+
}
162+
163+
function guardedByReversedTypeofNotObject(value: unknown): string {
164+
if ('object' !== typeof value) {
165+
return `Unexpected value: ${value}`; // Compliant
166+
}
167+
return '';
168+
}
169+
170+
function guardedByPrimitiveTypeofDisjunction(value: unknown): string | undefined {
171+
if (
172+
(typeof value === 'string' && value !== '') ||
173+
typeof value === 'number' ||
174+
typeof value === 'bigint'
175+
) {
176+
return 'value=' + value; // Compliant
177+
}
178+
return undefined;
179+
}
180+
181+
function guardedPlusEquals(value: unknown): string {
182+
let rendered = '';
183+
if (typeof value !== 'object') {
184+
rendered += value; // Compliant
185+
}
186+
return rendered;
187+
}
188+
189+
function guardedByTypeofNotObjectConjunction(value: unknown, label: string): string {
190+
if (typeof value !== 'object' && value !== null && value !== undefined) {
191+
return `${label}: ${value}`; // Compliant
192+
}
193+
return label;
194+
}
195+
196+
function guardedByTypeofObjectElse(value: unknown): string {
197+
if (typeof value === 'object') {
198+
return '';
199+
} else {
200+
return `Unexpected value: ${value}`; // Compliant
201+
}
202+
}
203+
204+
function guardedByReversedTypeofObjectElse(value: unknown): string {
205+
if ('object' === typeof value) {
206+
return '';
207+
} else {
208+
return `Unexpected value: ${value}`; // Compliant
209+
}
210+
}
211+
212+
function reassignedAfterTypeofGuardStillUnsafe(value: string | object, replacement: object): string {
213+
if (typeof value !== 'object') {
214+
value = replacement;
215+
return `Unexpected value: ${value}`; // Noncompliant {{'value' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
216+
}
217+
return '';
218+
}
219+
220+
function reassignedInsideTypeofGuardStillUnsafe(
221+
value: string | object,
222+
replacement: object,
223+
): string {
224+
if (typeof value !== 'object' && (value = replacement)) {
225+
return `Unexpected value: ${value}`; // Noncompliant {{'value' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
226+
}
227+
return '';
228+
}
229+
230+
function shadowedAfterTypeofGuardStillUnsafe(value: unknown, replacement: object): string {
231+
if (typeof value !== 'object') {
232+
const value = replacement;
233+
return `Unexpected value: ${value}`; // Noncompliant {{'value' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
234+
}
235+
return '';
236+
}
237+
238+
function truthinessGuardStillUnsafe(pluginName: object, uid: object): string {
239+
if (pluginName) {
240+
return `plugins::${pluginName}.${uid}`; // Noncompliant {{'pluginName' will use Object's default stringification format ('[object Object]') when stringified.}} {{'uid' will use Object's default stringification format ('[object Object]') when stringified.}}
241+
}
242+
return `application::${uid}`; // Noncompliant {{'uid' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
243+
}
244+
245+
function directToStringAfterPrimitiveGuardStillUnsafe(value: unknown): string | undefined {
246+
if (typeof value !== 'object') {
247+
return value.toString(); // Noncompliant {{'value' may use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
248+
}
249+
return undefined;
250+
}
251+
252+
function loopWriteAfterUseStillUnsafe(value: string | object, replacements: object[]): string {
253+
let rendered = '';
254+
if (typeof value !== 'object') {
255+
for (const replacement of replacements) {
256+
rendered += `${value}`; // Noncompliant {{'value' may use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
257+
value = replacement;
258+
}
259+
}
260+
return rendered;
261+
}
262+
263+
function arrayDestructuringWriteAfterTypeofGuardStillUnsafe(
264+
value: string | object,
265+
replacements: object[],
266+
): string {
267+
if (typeof value !== 'object') {
268+
[value] = replacements;
269+
return `Unexpected value: ${value}`; // Noncompliant {{'value' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
270+
}
271+
return '';
272+
}
273+
274+
function objectDestructuringWriteAfterTypeofGuardStillUnsafe(
275+
value: string | object,
276+
replacement: { value: object },
277+
): string {
278+
if (typeof value !== 'object') {
279+
({ value } = replacement);
280+
return `Unexpected value: ${value}`; // Noncompliant {{'value' will use Object's default stringification format ('[object Object]') when stringified.}} // NOSONAR S6551 - intentional noncompliant fixture case.
281+
}
282+
return '';
283+
}

packages/analysis/src/jsts/rules/S6551/cb.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,88 @@ function rejectedDefaultStringResultOnElse(data: unknown) {
118118
`,
119119
errors: 1,
120120
},
121+
{
122+
code: `
123+
function guardedByTypeofNotObject(value: unknown): string {
124+
if (typeof value !== 'object') {
125+
return \`Unexpected value: \${value}\`;
126+
}
127+
return '';
128+
}
129+
`,
130+
errors: 1,
131+
},
132+
{
133+
code: `
134+
function guardedByReversedTypeofNotObject(value: unknown): string {
135+
if ('object' !== typeof value) {
136+
return \`Unexpected value: \${value}\`;
137+
}
138+
return '';
139+
}
140+
`,
141+
errors: 1,
142+
},
143+
{
144+
code: `
145+
function guardedByTypeofNotObjectConjunction(value: unknown, label: string): string {
146+
if (typeof value !== 'object' && value !== null && value !== undefined) {
147+
return \`\${label}: \${value}\`;
148+
}
149+
return label;
150+
}
151+
`,
152+
errors: 1,
153+
},
154+
{
155+
code: `
156+
function guardedByTypeofObjectElse(value: unknown): string {
157+
if (typeof value === 'object') {
158+
return '';
159+
} else {
160+
return \`Unexpected value: \${value}\`;
161+
}
162+
}
163+
`,
164+
errors: 1,
165+
},
166+
{
167+
code: `
168+
function guardedByReversedTypeofObjectElse(value: unknown): string {
169+
if ('object' === typeof value) {
170+
return '';
171+
} else {
172+
return \`Unexpected value: \${value}\`;
173+
}
174+
}
175+
`,
176+
errors: 1,
177+
},
178+
],
179+
});
180+
});
181+
182+
it('upstream no-base-to-string reports wrapped operands on the wrapper node', () => {
183+
const ruleTester = new RuleTester();
184+
ruleTester.run('no-base-to-string', upstreamRule, {
185+
valid: [],
186+
invalid: [
187+
{
188+
code: `
189+
function wrappedValue(value: unknown): string {
190+
return \`Unexpected value: \${value as object}\`;
191+
}
192+
`,
193+
errors: [{ messageId: 'baseToString', type: 'TSAsExpression' }],
194+
},
195+
{
196+
code: `
197+
function nonNullValue(value: object | null): string {
198+
return \`Unexpected value: \${value!}\`;
199+
}
200+
`,
201+
errors: [{ messageId: 'baseToString', type: 'TSNonNullExpression' }],
202+
},
121203
],
122204
});
123205
});

0 commit comments

Comments
 (0)