Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This writes a `.claude/settings.json` with a `PostToolUse` hook that runs after

### Configuring Rules

By default, all 43 rules run. To customize, create a `laint.config.json` in your project root:
By default, all 44 rules run. To customize, create a `laint.config.json` in your project root:

```json
// Only run these specific rules (include mode)
Expand Down Expand Up @@ -88,7 +88,7 @@ const results = lintJsxCode(code, {
exclude: true,
});

// Run all 43 rules
// Run all 44 rules
const allResults = lintJsxCode(code, {
rules: [],
exclude: true,
Expand Down Expand Up @@ -117,7 +117,7 @@ const webRules = getRulesForPlatform('web');
const backendRules = getRulesForPlatform('backend');
```

## Available Rules (43 total)
## Available Rules (44 total)

### Expo Router Rules

Expand Down Expand Up @@ -203,6 +203,7 @@ const backendRules = getRulesForPlatform('backend');
| ------------------------ | -------- | --------- | ---------------------------------------------------------------- |
| `prefer-guard-clauses` | warning | universal | Use early returns instead of nesting if statements |
| `no-type-assertion` | warning | universal | Avoid `as` type casts; use type narrowing or proper types |
| `no-loose-equality` | warning | universal | Use === and !== instead of == and != (except == null) |
| `no-magic-env-strings` | warning | universal | Use centralized enum for env variable names, not magic strings |
| `no-nested-try-catch` | warning | universal | Avoid nested try-catch blocks, extract to separate functions |
| `no-string-coerce-error` | warning | universal | Use JSON.stringify instead of String() for unknown caught errors |
Expand Down Expand Up @@ -466,6 +467,26 @@ if (typeof data === 'string') {
const user: User = response.data;
```

### `no-loose-equality`

```typescript
// Bad - loose equality
if (a == b) {
}
if (x != 'hello') {
}

// Good - strict equality
if (a === b) {
}
if (x !== 'hello') {
}

// OK - == null is idiomatic for null/undefined check
if (value == null) {
}
```

### `no-magic-env-strings`

````typescript
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { transitionSharedTagMismatch } from './transition-shared-tag-mismatch';
import { transitionPreferBlankStack } from './transition-prefer-blank-stack';
import { preferGuardClauses } from './prefer-guard-clauses';
import { noTypeAssertion } from './no-type-assertion';
import { noLooseEquality } from './no-loose-equality';
import { noMagicEnvStrings } from './no-magic-env-strings';
import { urlParamsMustEncode } from './url-params-must-encode';
import { catchMustLogToSentry } from './catch-must-log-to-sentry';
Expand Down Expand Up @@ -77,6 +78,7 @@ export const rules: Record<string, RuleFunction> = {
'transition-prefer-blank-stack': transitionPreferBlankStack,
'prefer-guard-clauses': preferGuardClauses,
'no-type-assertion': noTypeAssertion,
'no-loose-equality': noLooseEquality,
'no-magic-env-strings': noMagicEnvStrings,
'url-params-must-encode': urlParamsMustEncode,
'catch-must-log-to-sentry': catchMustLogToSentry,
Expand Down
33 changes: 33 additions & 0 deletions src/rules/no-loose-equality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import type { File } from '@babel/types';
import type { LintResult } from '../types';

const RULE_NAME = 'no-loose-equality';

export function noLooseEquality(ast: File, _code: string): LintResult[] {
const results: LintResult[] = [];

traverse(ast, {
BinaryExpression(path) {
const { operator, left, right } = path.node;

if (operator !== '==' && operator !== '!=') return;

// Allow == null and != null (idiomatic null/undefined check)
if (t.isNullLiteral(right) || t.isNullLiteral(left)) return;

const strict = operator === '==' ? '===' : '!==';
const { loc } = path.node;
results.push({
rule: RULE_NAME,
message: `Use '${strict}' instead of '${operator}' for strict equality comparison.`,
line: loc?.start.line ?? 0,
column: loc?.start.column ?? 0,
severity: 'warning',
});
},
});

return results;
}
2 changes: 1 addition & 1 deletion tests/config-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('config modes', () => {
expect(ruleNames).toContain('expo-image-import');
expect(ruleNames).toContain('no-stylesheet-create');

expect(ruleNames.length).toBe(43);
expect(ruleNames.length).toBe(44);
});
});
});
76 changes: 76 additions & 0 deletions tests/no-loose-equality.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect } from 'vitest';
import { lintJsxCode } from '../src';

const RULE = 'no-loose-equality';

function lint(code: string) {
return lintJsxCode(code, { rules: [RULE] });
}

describe(RULE, () => {
it('flags == comparison', () => {
const code = `if (a == b) {}`;
const results = lint(code);
expect(results).toHaveLength(1);
expect(results[0].rule).toBe(RULE);
expect(results[0].message).toContain('===');
});

it('flags != comparison', () => {
const code = `if (a != b) {}`;
const results = lint(code);
expect(results).toHaveLength(1);
expect(results[0].message).toContain('!==');
});

it('flags == with string literal', () => {
const code = `if (x == 'hello') {}`;
const results = lint(code);
expect(results).toHaveLength(1);
});

it('flags == with number', () => {
const code = `if (count == 0) {}`;
const results = lint(code);
expect(results).toHaveLength(1);
});

it('allows == null (idiomatic null check)', () => {
const code = `if (value == null) {}`;
const results = lint(code);
expect(results).toHaveLength(0);
});

it('allows != null (idiomatic null check)', () => {
const code = `if (value != null) {}`;
const results = lint(code);
expect(results).toHaveLength(0);
});

it('allows null == value (reverse null check)', () => {
const code = `if (null == value) {}`;
const results = lint(code);
expect(results).toHaveLength(0);
});

it('allows === comparison', () => {
const code = `if (a === b) {}`;
const results = lint(code);
expect(results).toHaveLength(0);
});

it('allows !== comparison', () => {
const code = `if (a !== b) {}`;
const results = lint(code);
expect(results).toHaveLength(0);
});

it('flags multiple loose comparisons', () => {
const code = `
if (a == b) {}
if (c != d) {}
`;
const results = lint(code);
expect(results).toHaveLength(2);
});
});
Loading