Skip to content
Closed
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
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ import (
"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/use_unknown_in_catch_callback_variable"
"github.com/web-infra-dev/rslint/internal/rule"
"github.com/web-infra-dev/rslint/internal/rules/dot_notation"
"github.com/web-infra-dev/rslint/internal/rules/no_loss_of_precision"
"github.com/web-infra-dev/rslint/internal/rules/no_misleading_character_class"
"github.com/web-infra-dev/rslint/internal/rules/no_new_native_nonconstructor"
)

// RslintConfig represents the top-level configuration array
Expand Down Expand Up @@ -383,6 +386,11 @@ func registerAllTypeScriptEslintPluginRules() {
GlobalRuleRegistry.Register("@typescript-eslint/switch-exhaustiveness-check", switch_exhaustiveness_check.SwitchExhaustivenessCheckRule)
GlobalRuleRegistry.Register("@typescript-eslint/unbound-method", unbound_method.UnboundMethodRule)
GlobalRuleRegistry.Register("@typescript-eslint/use-unknown-in-catch-callback-variable", use_unknown_in_catch_callback_variable.UseUnknownInCatchCallbackVariableRule)

// Core ESLint rules
GlobalRuleRegistry.Register("no-loss-of-precision", no_loss_of_precision.NoLossOfPrecisionRule)
GlobalRuleRegistry.Register("no-misleading-character-class", no_misleading_character_class.NoMisleadingCharacterClassRule)
GlobalRuleRegistry.Register("no-new-native-nonconstructor", no_new_native_nonconstructor.NoNewNativeNonconstructorRule)
}

func registerAllEslintImportPluginRules() {
Expand Down
152 changes: 152 additions & 0 deletions internal/rules/no_loss_of_precision/no_loss_of_precision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package no_loss_of_precision

import (
"math"
"math/big"
"strconv"
"strings"

"github.com/microsoft/typescript-go/shim/ast"
"github.com/web-infra-dev/rslint/internal/rule"
"github.com/web-infra-dev/rslint/internal/utils"
)

// NoLossOfPrecisionRule implements the no-loss-of-precision rule
// Disallow number literals that lose precision
var NoLossOfPrecisionRule = rule.Rule{
Name: "no-loss-of-precision",
Run: run,
}

func run(ctx rule.RuleContext, options any) rule.RuleListeners {
return rule.RuleListeners{
ast.KindNumericLiteral: func(node *ast.Node) {
numLiteral := node.AsNumericLiteral()
if numLiteral == nil {
return
}

// Get the raw text of the numeric literal
rng := utils.TrimNodeTextRange(ctx.SourceFile, node)
rawText := ctx.SourceFile.Text()[rng.Pos():rng.End()]

// Remove numeric separators (underscores) if present
cleanText := strings.ReplaceAll(rawText, "_", "")

// Check if this literal loses precision
if losesPrecision(cleanText) {
ctx.ReportNode(node, rule.RuleMessage{
Id: "noLossOfPrecision",
Description: "This number literal will lose precision at runtime.",
})
}
},
}
}

// losesPrecision checks if a numeric literal loses precision when converted to float64
func losesPrecision(text string) bool {
// Handle different number formats
if strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X") {
return checkHexPrecision(text)
}
if strings.HasPrefix(text, "0b") || strings.HasPrefix(text, "0B") {
return checkBinaryPrecision(text)
}
if strings.HasPrefix(text, "0o") || strings.HasPrefix(text, "0O") {
return checkOctalPrecision(text)
}

// Handle decimal numbers (including scientific notation)
return checkDecimalPrecision(text)
}

// checkDecimalPrecision checks decimal numbers for precision loss
func checkDecimalPrecision(text string) bool {
// Parse as float64
floatVal, err := strconv.ParseFloat(text, 64)
if err != nil {
return false
}

// Special case: infinity means the number is too large
if math.IsInf(floatVal, 0) {
return true
}

// Use big.Float for arbitrary precision comparison
bigFloat := new(big.Float).SetPrec(1000) // High precision
_, _, err = bigFloat.Parse(text, 10)
if err != nil {
return false
}

// Convert the float64 back to big.Float
roundTrip := big.NewFloat(floatVal).SetPrec(1000)

// Compare: if they're not equal, precision was lost
return bigFloat.Cmp(roundTrip) != 0
}

// checkHexPrecision checks hexadecimal numbers for precision loss
func checkHexPrecision(text string) bool {
// Remove 0x prefix
hexStr := text[2:]

// Parse as big.Int
bigInt := new(big.Int)
_, success := bigInt.SetString(hexStr, 16)
if !success {
return false
}

// Check if it fits in safe integer range (2^53 - 1)
maxSafeInt := new(big.Int).SetInt64(9007199254740991) // 2^53 - 1
if bigInt.Cmp(maxSafeInt) > 0 {
return true
}

return false
}

// checkBinaryPrecision checks binary numbers for precision loss
func checkBinaryPrecision(text string) bool {
// Remove 0b prefix
binStr := text[2:]

// Parse as big.Int
bigInt := new(big.Int)
_, success := bigInt.SetString(binStr, 2)
if !success {
return false
}

// Check if it fits in safe integer range
maxSafeInt := new(big.Int).SetInt64(9007199254740991)
if bigInt.Cmp(maxSafeInt) > 0 {
return true
}

return false
}

// checkOctalPrecision checks octal numbers for precision loss
func checkOctalPrecision(text string) bool {
// Remove 0o prefix
octStr := text[2:]

// Parse as big.Int
bigInt := new(big.Int)
_, success := bigInt.SetString(octStr, 8)
if !success {
return false
}

// Check if it fits in safe integer range
maxSafeInt := new(big.Int).SetInt64(9007199254740991)
if bigInt.Cmp(maxSafeInt) > 0 {
return true
}

return false
}
101 changes: 101 additions & 0 deletions internal/rules/no_loss_of_precision/no_loss_of_precision_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package no_loss_of_precision

import (
"testing"

"github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/fixtures"
"github.com/web-infra-dev/rslint/internal/rule_tester"
)

func TestNoLossOfPrecisionRule(t *testing.T) {
rule_tester.RunRuleTester(
fixtures.GetRootDir(),
"tsconfig.json",
t,
&NoLossOfPrecisionRule,
[]rule_tester.ValidTestCase{
// Simple numbers
{Code: `var x = 12345;`},
{Code: `var x = 123.456;`},
{Code: `var x = -123.456;`},
{Code: `var x = 0;`},

// Scientific notation
{Code: `var x = 123e34;`},
{Code: `var x = 123e-34;`},
{Code: `var x = 12.3e-34;`},

// MAX_SAFE_INTEGER
{Code: `var x = 9007199254740991;`},

// Large safe integers
{Code: `var x = 12300000000000000000000000;`},

// Small decimals
{Code: `var x = 0.00000000000000000000000123;`},

// Binary, octal, hex
{Code: `var x = 0b11111111111111111111111111111111111111111111111111111;`},
{Code: `var x = 0o377777777777777777;`},
{Code: `var x = 0x1FFFFFFFFFFFFF;`},

// Non-numeric
{Code: `var x = true;`},
{Code: `var x = 'abc';`},
{Code: `var x = null;`},
},
[]rule_tester.InvalidTestCase{
// Integers with precision loss
{
Code: `var x = 9007199254740993;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},
{
Code: `var x = 5123000000000000000000000000001;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},

// Scientific notation with precision loss
{
Code: `var x = 9007199254740.993e3;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},
{
Code: `var x = 9.007199254740993e15;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},

// Decimals with precision loss
{
Code: `var x = 900719.9254740994;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},

// Hex with precision loss
{
Code: `var x = 0x20000000000001;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},

// Binary with precision loss
{
Code: `var x = 0b100000000000000000000000000000000000000000000000000001;`,
Errors: []rule_tester.InvalidTestCaseError{
{MessageId: "noLossOfPrecision"},
},
},
},
)
}
Loading
Loading