diff --git a/internal/config/config.go b/internal/config/config.go index be7eab1d4..96f91dfa0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,8 +14,10 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/array_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/await_thenable" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_ts_comment" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_tslint_comment" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/ban_types" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/class_literal_property_style" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/class_methods_use_this" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_generic_constructors" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_indexed_object_style" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_return" @@ -24,6 +26,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_exports" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/consistent_type_imports" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/default_param_last" + "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/explicit_function_return_type" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_array_delete" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_base_to_string" "github.com/web-infra-dev/rslint/internal/plugins/typescript/rules/no_confusing_void_expression" @@ -365,8 +368,10 @@ func registerAllTypeScriptEslintPluginRules() { GlobalRuleRegistry.Register("@typescript-eslint/array-type", array_type.ArrayTypeRule) GlobalRuleRegistry.Register("@typescript-eslint/await-thenable", await_thenable.AwaitThenableRule) GlobalRuleRegistry.Register("@typescript-eslint/ban-ts-comment", ban_ts_comment.BanTsCommentRule) + GlobalRuleRegistry.Register("@typescript-eslint/ban-tslint-comment", ban_tslint_comment.BanTslintCommentRule) GlobalRuleRegistry.Register("@typescript-eslint/ban-types", ban_types.BanTypesRule) GlobalRuleRegistry.Register("@typescript-eslint/class-literal-property-style", class_literal_property_style.ClassLiteralPropertyStyleRule) + GlobalRuleRegistry.Register("@typescript-eslint/class-methods-use-this", class_methods_use_this.ClassMethodsUseThisRule) GlobalRuleRegistry.Register("@typescript-eslint/consistent-generic-constructors", consistent_generic_constructors.ConsistentGenericConstructorsRule) GlobalRuleRegistry.Register("@typescript-eslint/consistent-indexed-object-style", consistent_indexed_object_style.ConsistentIndexedObjectStyleRule) GlobalRuleRegistry.Register("@typescript-eslint/consistent-return", consistent_return.ConsistentReturnRule) @@ -376,6 +381,7 @@ func registerAllTypeScriptEslintPluginRules() { GlobalRuleRegistry.Register("@typescript-eslint/consistent-type-imports", consistent_type_imports.ConsistentTypeImportsRule) GlobalRuleRegistry.Register("@typescript-eslint/default-param-last", default_param_last.DefaultParamLastRule) GlobalRuleRegistry.Register("@typescript-eslint/dot-notation", dot_notation.DotNotationRule) + GlobalRuleRegistry.Register("@typescript-eslint/explicit-function-return-type", explicit_function_return_type.ExplicitFunctionReturnTypeRule) GlobalRuleRegistry.Register("@typescript-eslint/no-array-delete", no_array_delete.NoArrayDeleteRule) GlobalRuleRegistry.Register("@typescript-eslint/no-base-to-string", no_base_to_string.NoBaseToStringRule) GlobalRuleRegistry.Register("@typescript-eslint/no-confusing-void-expression", no_confusing_void_expression.NoConfusingVoidExpressionRule) diff --git a/internal/linter/linter.go b/internal/linter/linter.go index f5e50722c..fab8b7a02 100644 --- a/internal/linter/linter.go +++ b/internal/linter/linter.go @@ -99,6 +99,20 @@ func RunLinterInProgram(program *compiler.Program, allowFiles []string, skipFile Severity: r.Severity, }) }, + ReportRangeWithFixes: func(textRange core.TextRange, msg rule.RuleMessage, fixes ...rule.RuleFix) { + // Check if rule is disabled at this position + if disableManager.IsRuleDisabled(r.Name, textRange.Pos()) { + return + } + onDiagnostic(rule.RuleDiagnostic{ + RuleName: r.Name, + Range: textRange, + Message: msg, + FixesPtr: &fixes, + SourceFile: file, + Severity: r.Severity, + }) + }, ReportNode: func(node *ast.Node, msg rule.RuleMessage) { // Check if rule is disabled at this position if disableManager.IsRuleDisabled(r.Name, node.Pos()) { diff --git a/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go new file mode 100644 index 000000000..245824e41 --- /dev/null +++ b/internal/plugins/typescript/rules/ban_tslint_comment/ban_tslint_comment.go @@ -0,0 +1,205 @@ +package ban_tslint_comment + +import ( + "regexp" + "strings" + + "github.com/microsoft/typescript-go/shim/core" + "github.com/web-infra-dev/rslint/internal/rule" +) + +// Regular expressions for matching TSLint directives +var ( + // Matches single-line comments: // tslint:disable or // tslint:enable + singleLineTslintRegex = regexp.MustCompile(`^//\s*tslint:(disable|enable)`) + + // Matches multi-line comments: /* tslint:disable */ or /* tslint:enable */ + multiLineTslintRegex = regexp.MustCompile(`^/\*\s*tslint:(disable|enable)`) +) + +// BanTslintCommentRule implements the ban-tslint-comment rule +// Bans // tslint: comments +var BanTslintCommentRule = rule.CreateRule(rule.Rule{ + Name: "ban-tslint-comment", + Run: run, +}) + +func run(ctx rule.RuleContext, options any) rule.RuleListeners { + // Get the full text of the source file + text := ctx.SourceFile.Text() + + // Process the text to find tslint comments + processComments(ctx, text) + + return rule.RuleListeners{} +} + +// processComments scans the source text for tslint comments +func processComments(ctx rule.RuleContext, text string) { + pos := 0 + length := len(text) + lineStarts := calculateLineStarts(text) + + for pos < length { + // Skip to next potential comment + if pos+1 < length { + if text[pos] == '/' && text[pos+1] == '/' { + // Single-line comment + commentStart := pos + pos += 2 + lineEnd := pos + for lineEnd < length && text[lineEnd] != '\n' && text[lineEnd] != '\r' { + lineEnd++ + } + commentText := text[commentStart:lineEnd] + + // Check if this is a tslint comment + if singleLineTslintRegex.MatchString(commentText) { + reportTslintComment(ctx, commentText, commentStart, lineEnd, lineStarts, text) + } + + pos = lineEnd + } else if text[pos] == '/' && text[pos+1] == '*' { + // Multi-line comment + commentStart := pos + pos += 2 + commentEnd := pos + for commentEnd+1 < length { + if text[commentEnd] == '*' && text[commentEnd+1] == '/' { + commentEnd += 2 + break + } + commentEnd++ + } + commentText := text[commentStart:commentEnd] + + // Check if this is a tslint comment + if multiLineTslintRegex.MatchString(commentText) { + reportTslintComment(ctx, commentText, commentStart, commentEnd, lineStarts, text) + } + + pos = commentEnd + } else { + pos++ + } + } else { + pos++ + } + } +} + +// calculateLineStarts returns the starting positions of each line +func calculateLineStarts(text string) []int { + lineStarts := []int{0} + for i := 0; i < len(text); i++ { + if text[i] == '\n' { + lineStarts = append(lineStarts, i+1) + } + } + return lineStarts +} + +// getLineAndColumn returns the line and column numbers for a given position +func getLineAndColumn(pos int, lineStarts []int) (line, column int) { + for i := len(lineStarts) - 1; i >= 0; i-- { + if pos >= lineStarts[i] { + line = i + 1 + column = pos - lineStarts[i] + 1 + return + } + } + return 1, 1 +} + +// reportTslintComment reports a tslint comment with autofix +func reportTslintComment(ctx rule.RuleContext, commentText string, start, end int, lineStarts []int, fullText string) { + line, column := getLineAndColumn(start, lineStarts) + + // Create the fix + fix := createFix(start, end, fullText) + + ctx.ReportRangeWithFixes( + core.NewTextRange(start, end), + rule.RuleMessage{ + Id: "commentDetected", + Description: "tslint is deprecated and you should stop using it", + }, + *fix, + ) + + _ = line + _ = column +} + +// createFix creates a fix that removes the tslint comment +func createFix(start, end int, fullText string) *rule.RuleFix { + // Check if we need to remove the entire line or just the comment + + // Look backwards to see if there's any non-whitespace before the comment + hasContentBefore := false + lineStart := start + for lineStart > 0 && fullText[lineStart-1] != '\n' && fullText[lineStart-1] != '\r' { + lineStart-- + if !isWhitespace(fullText[lineStart]) { + hasContentBefore = true + } + } + + // Look forwards to see if there's any non-whitespace after the comment (on the same line) + hasContentAfter := false + lineEnd := end + for lineEnd < len(fullText) && fullText[lineEnd] != '\n' && fullText[lineEnd] != '\r' { + if !isWhitespace(fullText[lineEnd]) { + hasContentAfter = true + break + } + lineEnd++ + } + + // Skip the newline characters if removing the entire line + if !hasContentBefore && !hasContentAfter { + // Include the newline in the removal + if lineEnd < len(fullText) && fullText[lineEnd] == '\r' { + lineEnd++ + } + if lineEnd < len(fullText) && fullText[lineEnd] == '\n' { + lineEnd++ + } + + return &rule.RuleFix{ + Range: core.NewTextRange(lineStart, lineEnd), + Text: "", + } + } + + // If there's content before the comment (e.g., "someCode(); // tslint:disable-line") + if hasContentBefore { + // Remove just the comment, preserving whitespace before it but removing the comment + // Find where the actual code ends + codeEnd := start + for codeEnd > lineStart && isWhitespace(fullText[codeEnd-1]) { + codeEnd-- + } + + return &rule.RuleFix{ + Range: core.NewTextRange(codeEnd, end), + Text: "", + } + } + + // Otherwise, just remove the comment + return &rule.RuleFix{ + Range: core.NewTextRange(start, end), + Text: "", + } +} + +// isWhitespace checks if a character is whitespace +func isWhitespace(ch byte) bool { + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' +} + +// trimTrailingWhitespace removes trailing whitespace and newlines +func trimTrailingWhitespace(s string) string { + return strings.TrimRight(s, " \t\r\n") +} diff --git a/internal/plugins/typescript/rules/class_methods_use_this/class_methods_use_this.go b/internal/plugins/typescript/rules/class_methods_use_this/class_methods_use_this.go new file mode 100644 index 000000000..e680045b9 --- /dev/null +++ b/internal/plugins/typescript/rules/class_methods_use_this/class_methods_use_this.go @@ -0,0 +1,504 @@ +package class_methods_use_this + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/web-infra-dev/rslint/internal/rule" + "github.com/web-infra-dev/rslint/internal/utils" +) + +type ClassMethodsUseThisOptions struct { + ExceptMethods []string `json:"exceptMethods"` + EnforceForClassFields bool `json:"enforceForClassFields"` + IgnoreClassesThatImplementAnInterface interface{} `json:"ignoreClassesThatImplementAnInterface"` + IgnoreOverrideMethods bool `json:"ignoreOverrideMethods"` +} + +type scopeInfo struct { + hasThis bool + node *ast.Node + upper *scopeInfo +} + +var ClassMethodsUseThisRule = rule.CreateRule(rule.Rule{ + Name: "class-methods-use-this", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + opts := ClassMethodsUseThisOptions{ + ExceptMethods: []string{}, + EnforceForClassFields: true, + IgnoreClassesThatImplementAnInterface: false, + IgnoreOverrideMethods: false, + } + + // Parse options + if options != nil { + var optsMap map[string]interface{} + if optsArray, ok := options.([]interface{}); ok && len(optsArray) > 0 { + if opts, ok := optsArray[0].(map[string]interface{}); ok { + optsMap = opts + } + } else if opts, ok := options.(map[string]interface{}); ok { + optsMap = opts + } + + if optsMap != nil { + if exceptMethods, ok := optsMap["exceptMethods"].([]interface{}); ok { + for _, method := range exceptMethods { + if str, ok := method.(string); ok { + opts.ExceptMethods = append(opts.ExceptMethods, str) + } + } + } + if enforceForClassFields, ok := optsMap["enforceForClassFields"].(bool); ok { + opts.EnforceForClassFields = enforceForClassFields + } + if ignoreClasses, ok := optsMap["ignoreClassesThatImplementAnInterface"]; ok { + opts.IgnoreClassesThatImplementAnInterface = ignoreClasses + } + if ignoreOverride, ok := optsMap["ignoreOverrideMethods"].(bool); ok { + opts.IgnoreOverrideMethods = ignoreOverride + } + } + } + + // Helper to check if a method name is excepted + isExceptedMethod := func(methodName string) bool { + for _, name := range opts.ExceptMethods { + if name == methodName { + return true + } + } + return false + } + + // Helper to check if node is inside a class + isInClass := func(node *ast.Node) bool { + current := node.Parent + for current != nil { + if current.Kind == ast.KindClassDeclaration || current.Kind == ast.KindClassExpression { + return true + } + current = current.Parent + } + return false + } + + // Helper to get the parent class node + getParentClass := func(node *ast.Node) *ast.Node { + current := node.Parent + for current != nil { + if current.Kind == ast.KindClassDeclaration || current.Kind == ast.KindClassExpression { + return current + } + current = current.Parent + } + return nil + } + + // Helper to check if a class implements an interface + classImplementsInterface := func(classNode *ast.Node) bool { + if classNode == nil { + return false + } + + heritageClauses := utils.GetHeritageClauses(classNode) + if heritageClauses == nil || len(heritageClauses.Nodes) == 0 { + return false + } + + for _, clauseNode := range heritageClauses.Nodes { + clause := clauseNode.AsHeritageClause() + if clause != nil && clause.Token == ast.KindImplementsKeyword { + return true + } + } + return false + } + + // Helper to check if member should be ignored based on ignoreClassesThatImplementAnInterface option + shouldIgnoreInterfaceImpl := func(node *ast.Node) bool { + if opts.IgnoreClassesThatImplementAnInterface == nil || opts.IgnoreClassesThatImplementAnInterface == false { + return false + } + + classNode := getParentClass(node) + if !classImplementsInterface(classNode) { + return false + } + + // If option is true, ignore all members of classes that implement interfaces + if boolVal, ok := opts.IgnoreClassesThatImplementAnInterface.(bool); ok && boolVal { + return true + } + + // If option is "public-fields", only ignore public members + if strVal, ok := opts.IgnoreClassesThatImplementAnInterface.(string); ok && strVal == "public-fields" { + // Check if the member is private or protected + hasPrivateModifier := ast.HasSyntacticModifier(node, ast.ModifierFlagsPrivate) + hasProtectedModifier := ast.HasSyntacticModifier(node, ast.ModifierFlagsProtected) + isPrivateName := false + + // Check if it's a private name (starts with #) + if node.Kind == ast.KindMethodDeclaration { + if method := node.AsMethodDeclaration(); method != nil && method.Name() != nil { + _, nameType := utils.GetNameFromMember(ctx.SourceFile, method.Name()) + isPrivateName = nameType == utils.MemberNameTypePrivate + } + } else if node.Kind == ast.KindPropertyDeclaration { + if prop := node.AsPropertyDeclaration(); prop != nil && prop.Name() != nil { + _, nameType := utils.GetNameFromMember(ctx.SourceFile, prop.Name()) + isPrivateName = nameType == utils.MemberNameTypePrivate + } + } else if node.Kind == ast.KindGetAccessor { + if accessor := node.AsGetAccessorDeclaration(); accessor != nil && accessor.Name() != nil { + _, nameType := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + isPrivateName = nameType == utils.MemberNameTypePrivate + } + } else if node.Kind == ast.KindSetAccessor { + if accessor := node.AsSetAccessorDeclaration(); accessor != nil && accessor.Name() != nil { + _, nameType := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + isPrivateName = nameType == utils.MemberNameTypePrivate + } + } + + // If it's private or protected, don't ignore it (check it) + if hasPrivateModifier || hasProtectedModifier || isPrivateName { + return false + } + + // It's a public member, so ignore it + return true + } + + return false + } + + // Helper to check if member has override modifier + hasOverrideModifier := func(node *ast.Node) bool { + return ast.HasSyntacticModifier(node, ast.ModifierFlagsOverride) + } + + // Get method name for display + getMethodName := func(node *ast.Node) string { + if node.Kind == ast.KindMethodDeclaration { + method := node.AsMethodDeclaration() + if method != nil && method.Name() != nil { + name, nameType := utils.GetNameFromMember(ctx.SourceFile, method.Name()) + if nameType == utils.MemberNameTypePrivate { + if method.Kind == ast.KindGetAccessor { + return "private getter " + name + } else if method.Kind == ast.KindSetAccessor { + return "private setter " + name + } else if method.AsteriskToken != nil { + return "private generator method " + name + } + return "private method " + name + } + + if method.Kind == ast.KindGetAccessor { + if name == "" { + return "getter" + } + return "getter '" + name + "'" + } else if method.Kind == ast.KindSetAccessor { + if name == "" { + return "setter" + } + return "setter '" + name + "'" + } else if method.AsteriskToken != nil { + if name == "" { + return "generator method" + } + return "generator method '" + name + "'" + } + if name == "" { + return "method" + } + return "method '" + name + "'" + } + return "method" + } else if node.Kind == ast.KindGetAccessor { + accessor := node.AsGetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + name, nameType := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + if nameType == utils.MemberNameTypePrivate { + return "private getter " + name + } + if name == "" { + return "getter" + } + return "getter '" + name + "'" + } + return "getter" + } else if node.Kind == ast.KindSetAccessor { + accessor := node.AsSetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + name, nameType := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + if nameType == utils.MemberNameTypePrivate { + return "private setter " + name + } + if name == "" { + return "setter" + } + return "setter '" + name + "'" + } + return "setter" + } else if node.Kind == ast.KindPropertyDeclaration { + prop := node.AsPropertyDeclaration() + if prop != nil && prop.Name() != nil { + name, nameType := utils.GetNameFromMember(ctx.SourceFile, prop.Name()) + if nameType == utils.MemberNameTypePrivate { + return "private method " + name + } + if name == "" { + return "method" + } + return "method '" + name + "'" + } + return "method" + } + return "method" + } + + var currentScope *scopeInfo + + // Enter a method or property + enterMethod := func(node *ast.Node) { + // Skip constructors + if node.Kind == ast.KindConstructor { + return + } + + // Skip static methods + if ast.HasSyntacticModifier(node, ast.ModifierFlagsStatic) { + return + } + + // Skip abstract methods + if ast.HasSyntacticModifier(node, ast.ModifierFlagsAbstract) { + return + } + + // Skip if not in a class + if !isInClass(node) { + return + } + + // Skip if has override modifier and ignoreOverrideMethods is true + if opts.IgnoreOverrideMethods && hasOverrideModifier(node) { + return + } + + // Skip if in a class that implements an interface and should be ignored + if shouldIgnoreInterfaceImpl(node) { + return + } + + // Check if method is in except list + var methodName string + if node.Kind == ast.KindMethodDeclaration { + method := node.AsMethodDeclaration() + if method != nil && method.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, method.Name()) + methodName = name + } + } else if node.Kind == ast.KindGetAccessor { + accessor := node.AsGetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + methodName = name + } + } else if node.Kind == ast.KindSetAccessor { + accessor := node.AsSetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + methodName = name + } + } + + if methodName != "" && isExceptedMethod(methodName) { + return + } + + // Create a new scope + currentScope = &scopeInfo{ + hasThis: false, + node: node, + upper: currentScope, + } + } + + // Exit a method + exitMethod := func(node *ast.Node) { + if currentScope != nil && currentScope.node == node { + // Check if we used 'this' or 'super' + if !currentScope.hasThis { + displayName := getMethodName(node) + ctx.ReportNode(node, rule.RuleMessage{ + Id: "missingThis", + Description: "Expected 'this' to be used by class " + displayName + ".", + }) + } + + // Pop the scope + currentScope = currentScope.upper + } + } + + // Enter a property initializer (function or arrow function) + enterPropertyInit := func(node *ast.Node) { + // Only process if this is a function/arrow that's a direct child of a property declaration + parent := node.Parent + if parent == nil || parent.Kind != ast.KindPropertyDeclaration { + return + } + + prop := parent.AsPropertyDeclaration() + if prop == nil || prop.Initializer != node { + return + } + + // Skip if enforceForClassFields is false + if !opts.EnforceForClassFields { + return + } + + // Skip static properties + if ast.HasSyntacticModifier(parent, ast.ModifierFlagsStatic) { + return + } + + // Skip if not in a class + if !isInClass(parent) { + return + } + + // Skip if has override modifier and ignoreOverrideMethods is true + if opts.IgnoreOverrideMethods && hasOverrideModifier(parent) { + return + } + + // Skip if in a class that implements an interface and should be ignored + if shouldIgnoreInterfaceImpl(parent) { + return + } + + // Check if property name is in except list + if prop.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, prop.Name()) + if name != "" && isExceptedMethod(name) { + return + } + } + + // Create a new scope for the initializer + currentScope = &scopeInfo{ + hasThis: false, + node: node, + upper: currentScope, + } + } + + // Exit a property initializer + exitPropertyInit := func(node *ast.Node) { + if currentScope != nil && currentScope.node == node { + // Check if we used 'this' or 'super' + if !currentScope.hasThis { + // Find the parent property declaration + parent := node.Parent + if parent != nil && parent.Kind == ast.KindPropertyDeclaration { + displayName := getMethodName(parent) + ctx.ReportNode(node, rule.RuleMessage{ + Id: "missingThis", + Description: "Expected 'this' to be used by class " + displayName + ".", + }) + } + } + + // Pop the scope + currentScope = currentScope.upper + } + } + + // Mark that we found 'this' or 'super' + markAsHasThis := func() { + if currentScope != nil { + currentScope.hasThis = true + } + } + + // Enter a function expression or arrow function (to create a boundary for nested functions) + enterNestedFunction := func(node *ast.Node) { + // Check if this is a property initializer - if so, skip (handled separately) + parent := node.Parent + if parent != nil && parent.Kind == ast.KindPropertyDeclaration { + prop := parent.AsPropertyDeclaration() + if prop != nil && prop.Initializer == node { + return + } + } + + // Don't check nested regular functions for 'this' + // Create a boundary scope for nested functions + if node.Kind == ast.KindFunctionExpression || node.Kind == ast.KindFunctionDeclaration { + currentScope = &scopeInfo{ + hasThis: true, // Mark as having 'this' so we don't report + node: node, + upper: currentScope, + } + } + } + + // Exit a nested function + exitNestedFunction := func(node *ast.Node) { + // Skip if this is a property initializer (handled by exitPropertyInit) + parent := node.Parent + if parent != nil && parent.Kind == ast.KindPropertyDeclaration { + prop := parent.AsPropertyDeclaration() + if prop != nil && prop.Initializer == node { + return + } + } + + if currentScope != nil && currentScope.node == node { + currentScope = currentScope.upper + } + } + + return rule.RuleListeners{ + // Method listeners + ast.KindMethodDeclaration: enterMethod, + rule.ListenerOnExit(ast.KindMethodDeclaration): exitMethod, + ast.KindGetAccessor: enterMethod, + rule.ListenerOnExit(ast.KindGetAccessor): exitMethod, + ast.KindSetAccessor: enterMethod, + rule.ListenerOnExit(ast.KindSetAccessor): exitMethod, + + // Function expression/arrow function listeners + // These handle both property initializers and nested functions + ast.KindFunctionExpression: func(node *ast.Node) { + enterPropertyInit(node) + enterNestedFunction(node) + }, + rule.ListenerOnExit(ast.KindFunctionExpression): func(node *ast.Node) { + exitPropertyInit(node) + exitNestedFunction(node) + }, + ast.KindFunctionDeclaration: func(node *ast.Node) { + enterNestedFunction(node) + }, + rule.ListenerOnExit(ast.KindFunctionDeclaration): func(node *ast.Node) { + exitNestedFunction(node) + }, + ast.KindArrowFunction: func(node *ast.Node) { + enterPropertyInit(node) + }, + rule.ListenerOnExit(ast.KindArrowFunction): func(node *ast.Node) { + exitPropertyInit(node) + }, + + // This/super keyword listeners + ast.KindThisKeyword: func(node *ast.Node) { markAsHasThis() }, + ast.KindSuperKeyword: func(node *ast.Node) { markAsHasThis() }, + } + }, +}) diff --git a/internal/plugins/typescript/rules/explicit_function_return_type/explicit_function_return_type.go b/internal/plugins/typescript/rules/explicit_function_return_type/explicit_function_return_type.go new file mode 100644 index 000000000..3d37ce288 --- /dev/null +++ b/internal/plugins/typescript/rules/explicit_function_return_type/explicit_function_return_type.go @@ -0,0 +1,573 @@ +// Package explicit_function_return_type implements the @typescript-eslint/explicit-function-return-type rule. +// This rule enforces explicit return type annotations on functions and methods, +// improving code documentation and type safety by requiring developers to explicitly +// declare what types their functions return. +package explicit_function_return_type + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/web-infra-dev/rslint/internal/rule" + "github.com/web-infra-dev/rslint/internal/utils" +) + +type ExplicitFunctionReturnTypeOptions struct { + AllowExpressions bool `json:"allowExpressions"` + AllowTypedFunctionExpressions bool `json:"allowTypedFunctionExpressions"` + AllowHigherOrderFunctions bool `json:"allowHigherOrderFunctions"` + AllowDirectConstAssertionInArrowFunctions bool `json:"allowDirectConstAssertionInArrowFunctions"` + AllowConciseArrowFunctionExpressionsStartingWithVoid bool `json:"allowConciseArrowFunctionExpressionsStartingWithVoid"` + AllowFunctionsWithoutTypeParameters bool `json:"allowFunctionsWithoutTypeParameters"` + AllowIIFEs bool `json:"allowIIFEs"` + AllowedNames []string `json:"allowedNames"` +} + +func parseOptions(options any) ExplicitFunctionReturnTypeOptions { + opts := ExplicitFunctionReturnTypeOptions{ + AllowExpressions: false, + AllowTypedFunctionExpressions: true, + AllowHigherOrderFunctions: true, + AllowDirectConstAssertionInArrowFunctions: true, + AllowConciseArrowFunctionExpressionsStartingWithVoid: false, + AllowFunctionsWithoutTypeParameters: false, + AllowIIFEs: false, + AllowedNames: []string{}, + } + + if options == nil { + return opts + } + + var optsMap map[string]interface{} + if optsArray, ok := options.([]interface{}); ok && len(optsArray) > 0 { + if m, ok := optsArray[0].(map[string]interface{}); ok { + optsMap = m + } + } else if m, ok := options.(map[string]interface{}); ok { + optsMap = m + } + + if optsMap != nil { + if v, ok := optsMap["allowExpressions"].(bool); ok { + opts.AllowExpressions = v + } + if v, ok := optsMap["allowTypedFunctionExpressions"].(bool); ok { + opts.AllowTypedFunctionExpressions = v + } + if v, ok := optsMap["allowHigherOrderFunctions"].(bool); ok { + opts.AllowHigherOrderFunctions = v + } + if v, ok := optsMap["allowDirectConstAssertionInArrowFunctions"].(bool); ok { + opts.AllowDirectConstAssertionInArrowFunctions = v + } + if v, ok := optsMap["allowConciseArrowFunctionExpressionsStartingWithVoid"].(bool); ok { + opts.AllowConciseArrowFunctionExpressionsStartingWithVoid = v + } + if v, ok := optsMap["allowFunctionsWithoutTypeParameters"].(bool); ok { + opts.AllowFunctionsWithoutTypeParameters = v + } + if v, ok := optsMap["allowIIFEs"].(bool); ok { + opts.AllowIIFEs = v + } + if allowedNames, ok := optsMap["allowedNames"].([]interface{}); ok { + for _, name := range allowedNames { + if str, ok := name.(string); ok { + opts.AllowedNames = append(opts.AllowedNames, str) + } + } + } + } + + return opts +} + +func buildMissingReturnTypeMessage() rule.RuleMessage { + return rule.RuleMessage{ + Id: "missingReturnType", + Description: "Missing return type on function.", + } +} + +// Check if a function has an explicit return type +func hasReturnType(node *ast.Node) bool { + switch node.Kind { + case ast.KindFunctionDeclaration: + fn := node.AsFunctionDeclaration() + return fn != nil && fn.Type != nil + case ast.KindFunctionExpression: + fn := node.AsFunctionExpression() + return fn != nil && fn.Type != nil + case ast.KindArrowFunction: + fn := node.AsArrowFunction() + return fn != nil && fn.Type != nil + case ast.KindMethodDeclaration: + method := node.AsMethodDeclaration() + return method != nil && method.Type != nil + case ast.KindGetAccessor: + accessor := node.AsGetAccessorDeclaration() + return accessor != nil && accessor.Type != nil + } + return false +} + +// Check if function has type parameters +func hasTypeParameters(node *ast.Node) bool { + switch node.Kind { + case ast.KindFunctionDeclaration: + fn := node.AsFunctionDeclaration() + return fn != nil && fn.TypeParameters != nil && fn.TypeParameters.Nodes != nil && len(fn.TypeParameters.Nodes) > 0 + case ast.KindFunctionExpression: + fn := node.AsFunctionExpression() + return fn != nil && fn.TypeParameters != nil && fn.TypeParameters.Nodes != nil && len(fn.TypeParameters.Nodes) > 0 + case ast.KindArrowFunction: + fn := node.AsArrowFunction() + return fn != nil && fn.TypeParameters != nil && fn.TypeParameters.Nodes != nil && len(fn.TypeParameters.Nodes) > 0 + case ast.KindMethodDeclaration: + method := node.AsMethodDeclaration() + return method != nil && method.TypeParameters != nil && method.TypeParameters.Nodes != nil && len(method.TypeParameters.Nodes) > 0 + } + return false +} + +// Check if arrow function body is a const assertion +func isConstAssertion(node *ast.Node) bool { + if node == nil { + return false + } + + // Check for direct const assertion: () => x as const + if node.Kind == ast.KindAsExpression { + asExpr := node.AsAsExpression() + if asExpr != nil && asExpr.Type != nil && asExpr.Type.Kind == ast.KindTypeReference { + typeRef := asExpr.Type.AsTypeReference() + if typeRef != nil && ast.IsIdentifier(typeRef.TypeName) { + ident := typeRef.TypeName.AsIdentifier() + if ident != nil && ident.Text == "const" { + return true + } + } + } + } + + // Check for satisfies with const: () => x as const satisfies R + if node.Kind == ast.KindSatisfiesExpression { + satisfiesExpr := node.AsSatisfiesExpression() + if satisfiesExpr != nil && satisfiesExpr.Expression != nil { + return isConstAssertion(satisfiesExpr.Expression) + } + } + + return false +} + +// Check if arrow function starts with void +func startsWithVoid(node *ast.Node) bool { + if node == nil || node.Kind != ast.KindArrowFunction { + return false + } + + arrowFn := node.AsArrowFunction() + if arrowFn == nil || arrowFn.Body == nil { + return false + } + + // Check if body is a void expression + if arrowFn.Body.Kind == ast.KindVoidExpression { + return true + } + + return false +} + +// Check if node is a typed function expression +func isTypedFunctionExpression(ctx rule.RuleContext, node *ast.Node) bool { + parent := node.Parent + if parent == nil { + return false + } + + // Check for variable declaration with type: const x: Foo = () => {} + if parent.Kind == ast.KindVariableDeclaration { + varDecl := parent.AsVariableDeclaration() + if varDecl != nil && varDecl.Type != nil { + return true + } + } + + // Check for type assertion: (() => {}) as Foo or (() => {}) + if parent.Kind == ast.KindAsExpression || parent.Kind == ast.KindTypeAssertionExpression { + return true + } + + // Check for property assignment in typed object literal + if parent.Kind == ast.KindPropertyAssignment { + // Walk up to find if object literal has type assertion + for p := parent.Parent; p != nil; p = p.Parent { + if p.Kind == ast.KindAsExpression || p.Kind == ast.KindTypeAssertionExpression { + return true + } + if p.Kind == ast.KindVariableDeclaration { + varDecl := p.AsVariableDeclaration() + if varDecl != nil && varDecl.Type != nil { + return true + } + } + // Stop at certain boundaries + if p.Kind == ast.KindSourceFile || p.Kind == ast.KindBlock { + break + } + } + } + + // Check for property declaration with type: private method: MethodType = () => {} + if parent.Kind == ast.KindPropertyDeclaration { + propDecl := parent.AsPropertyDeclaration() + if propDecl != nil && propDecl.Type != nil { + return true + } + } + + // Note: Without full type system access, we cannot accurately determine if a function + // passed as an argument to a call expression or used in JSX has a typed signature + // from the parameter's type annotation. This may lead to false positives where + // functions that should be flagged are incorrectly considered typed. + // Examples that would require type information: + // - setTimeout(() => {}) - callback is NOT typed + // - array.map((x) => {}) - callback parameter type comes from array element type + // - {}} /> - prop type comes from component definition + + return false +} + +// Check if node is a higher-order function (returns a function) +// Note: This only checks for direct returns of functions. It does not handle: +// - Conditional returns: () => condition ? () => {} : null +// - Wrapped returns: () => Promise.resolve(() => {}) +// - Multiple return paths with different types +// These limitations may cause some higher-order functions to not be detected. +func isHigherOrderFunction(node *ast.Node) bool { + if node == nil { + return false + } + + switch node.Kind { + case ast.KindArrowFunction: + arrowFn := node.AsArrowFunction() + if arrowFn == nil || arrowFn.Body == nil { + return false + } + + // Direct return of arrow or function: () => () => {} + bodyKind := arrowFn.Body.Kind + if bodyKind == ast.KindArrowFunction || bodyKind == ast.KindFunctionExpression { + return true + } + + // Block with return statement + if bodyKind == ast.KindBlock { + block := arrowFn.Body.AsBlock() + if block != nil && block.Statements != nil && block.Statements.Nodes != nil { + for _, stmt := range block.Statements.Nodes { + if stmt.Kind == ast.KindReturnStatement { + retStmt := stmt.AsReturnStatement() + if retStmt != nil && retStmt.Expression != nil { + exprKind := retStmt.Expression.Kind + if exprKind == ast.KindArrowFunction || exprKind == ast.KindFunctionExpression { + return true + } + } + } + } + } + } + + case ast.KindFunctionDeclaration, ast.KindFunctionExpression: + var body *ast.Node + if node.Kind == ast.KindFunctionDeclaration { + fn := node.AsFunctionDeclaration() + if fn != nil { + body = fn.Body + } + } else { + fn := node.AsFunctionExpression() + if fn != nil { + body = fn.Body + } + } + + if body == nil || body.Kind != ast.KindBlock { + return false + } + + block := body.AsBlock() + if block != nil && block.Statements != nil && block.Statements.Nodes != nil { + for _, stmt := range block.Statements.Nodes { + if stmt.Kind == ast.KindReturnStatement { + retStmt := stmt.AsReturnStatement() + if retStmt != nil && retStmt.Expression != nil { + exprKind := retStmt.Expression.Kind + if exprKind == ast.KindArrowFunction || exprKind == ast.KindFunctionExpression { + return true + } + } + } + } + } + } + + return false +} + +// Check if node is an IIFE (Immediately Invoked Function Expression) +func isIIFE(node *ast.Node) bool { + if node == nil { + return false + } + + parent := node.Parent + if parent == nil { + return false + } + + // Check if parent is a call expression + if parent.Kind == ast.KindCallExpression { + callExpr := parent.AsCallExpression() + if callExpr != nil && callExpr.Expression == node { + return true + } + } + + // Check for parenthesized IIFE: (function() {})() + if parent.Kind == ast.KindParenthesizedExpression { + grandparent := parent.Parent + if grandparent != nil && grandparent.Kind == ast.KindCallExpression { + return true + } + } + + return false +} + +// Check if function is used as an expression +func isExpression(node *ast.Node) bool { + if node == nil { + return false + } + + parent := node.Parent + if parent == nil { + return false + } + + // Function declarations are not expressions + if node.Kind == ast.KindFunctionDeclaration { + return false + } + + // Check various expression contexts + switch parent.Kind { + case ast.KindCallExpression, ast.KindNewExpression: + return true + case ast.KindArrayLiteralExpression: + return true + case ast.KindParenthesizedExpression: + return true + case ast.KindBinaryExpression: + return true + case ast.KindConditionalExpression: + return true + case ast.KindExportAssignment: + return true + case ast.KindReturnStatement: + return true + case ast.KindJsxExpression, ast.KindJsxAttribute: + return true + case ast.KindPropertyAssignment: + return true + } + + return false +} + +// Get function name for reporting +func getFunctionName(ctx rule.RuleContext, node *ast.Node) string { + switch node.Kind { + case ast.KindFunctionDeclaration: + fn := node.AsFunctionDeclaration() + if fn != nil && fn.Name() != nil && fn.Name().Kind == ast.KindIdentifier { + ident := fn.Name().AsIdentifier() + if ident != nil { + return ident.Text + } + } + case ast.KindFunctionExpression: + fn := node.AsFunctionExpression() + if fn != nil && fn.Name() != nil && fn.Name().Kind == ast.KindIdentifier { + ident := fn.Name().AsIdentifier() + if ident != nil { + return ident.Text + } + } + // Check parent for name + if node.Parent != nil && node.Parent.Kind == ast.KindVariableDeclaration { + varDecl := node.Parent.AsVariableDeclaration() + if varDecl != nil && varDecl.Name() != nil && varDecl.Name().Kind == ast.KindIdentifier { + ident := varDecl.Name().AsIdentifier() + if ident != nil { + return ident.Text + } + } + } + case ast.KindArrowFunction: + // Check parent for name + if node.Parent != nil && node.Parent.Kind == ast.KindVariableDeclaration { + varDecl := node.Parent.AsVariableDeclaration() + if varDecl != nil && varDecl.Name() != nil && varDecl.Name().Kind == ast.KindIdentifier { + ident := varDecl.Name().AsIdentifier() + if ident != nil { + return ident.Text + } + } + } + case ast.KindMethodDeclaration: + method := node.AsMethodDeclaration() + if method != nil && method.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, method.Name()) + return name + } + case ast.KindGetAccessor: + accessor := node.AsGetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + name, _ := utils.GetNameFromMember(ctx.SourceFile, accessor.Name()) + return name + } + } + return "" +} + +// Check if function name is in allowed list +func isAllowedName(ctx rule.RuleContext, node *ast.Node, allowedNames []string) bool { + if len(allowedNames) == 0 { + return false + } + + name := getFunctionName(ctx, node) + if name == "" { + return false + } + + for _, allowed := range allowedNames { + if name == allowed { + return true + } + } + return false +} + +// Get the node to report (the function signature part) +func getReportNode(node *ast.Node) *ast.Node { + switch node.Kind { + case ast.KindFunctionDeclaration: + fn := node.AsFunctionDeclaration() + if fn != nil && fn.Name() != nil { + return fn.Name() + } + case ast.KindFunctionExpression: + // Return the "function" keyword position + return node + case ast.KindArrowFunction: + // Return the arrow function node itself for positioning + return node + case ast.KindMethodDeclaration: + method := node.AsMethodDeclaration() + if method != nil && method.Name() != nil { + return method.Name() + } + case ast.KindGetAccessor: + accessor := node.AsGetAccessorDeclaration() + if accessor != nil && accessor.Name() != nil { + return accessor.Name() + } + } + return node +} + +// Check if function should be skipped based on options +func shouldSkipFunction(ctx rule.RuleContext, node *ast.Node, opts ExplicitFunctionReturnTypeOptions) bool { + // Check allowedNames option + if isAllowedName(ctx, node, opts.AllowedNames) { + return true + } + + // Check allowFunctionsWithoutTypeParameters option + if opts.AllowFunctionsWithoutTypeParameters && !hasTypeParameters(node) { + return true + } + + // Check allowExpressions option + if opts.AllowExpressions && isExpression(node) { + return true + } + + // Check allowTypedFunctionExpressions option + if opts.AllowTypedFunctionExpressions && + (node.Kind == ast.KindFunctionExpression || node.Kind == ast.KindArrowFunction) && + isTypedFunctionExpression(ctx, node) { + return true + } + + // Check allowHigherOrderFunctions option + if opts.AllowHigherOrderFunctions && isHigherOrderFunction(node) { + return true + } + + // Check allowDirectConstAssertionInArrowFunctions option + if opts.AllowDirectConstAssertionInArrowFunctions && node.Kind == ast.KindArrowFunction { + arrowFn := node.AsArrowFunction() + if arrowFn != nil && isConstAssertion(arrowFn.Body) { + return true + } + } + + // Check allowConciseArrowFunctionExpressionsStartingWithVoid option + if opts.AllowConciseArrowFunctionExpressionsStartingWithVoid && startsWithVoid(node) { + return true + } + + // Check allowIIFEs option + if opts.AllowIIFEs && isIIFE(node) { + return true + } + + return false +} + +var ExplicitFunctionReturnTypeRule = rule.CreateRule(rule.Rule{ + Name: "explicit-function-return-type", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + opts := parseOptions(options) + + checkFunction := func(node *ast.Node) { + // Skip if already has return type + if hasReturnType(node) { + return + } + + // Skip if any of the options indicate this function should be ignored + if shouldSkipFunction(ctx, node, opts) { + return + } + + // Report the missing return type + reportNode := getReportNode(node) + ctx.ReportNode(reportNode, buildMissingReturnTypeMessage()) + } + + return rule.RuleListeners{ + ast.KindFunctionDeclaration: checkFunction, + ast.KindFunctionExpression: checkFunction, + ast.KindArrowFunction: checkFunction, + ast.KindMethodDeclaration: checkFunction, + ast.KindGetAccessor: checkFunction, + } + }, +}) diff --git a/internal/rule/rule.go b/internal/rule/rule.go index d533a25d7..cec1e9b34 100644 --- a/internal/rule/rule.go +++ b/internal/rule/rule.go @@ -168,6 +168,7 @@ type RuleContext struct { DisableManager *DisableManager ReportRange func(textRange core.TextRange, msg RuleMessage) ReportRangeWithSuggestions func(textRange core.TextRange, msg RuleMessage, suggestions ...RuleSuggestion) + ReportRangeWithFixes func(textRange core.TextRange, msg RuleMessage, fixes ...RuleFix) ReportNode func(node *ast.Node, msg RuleMessage) ReportNodeWithFixes func(node *ast.Node, msg RuleMessage, fixes ...RuleFix) ReportNodeWithSuggestions func(node *ast.Node, msg RuleMessage, suggestions ...RuleSuggestion) diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index 55466041e..45bb49eb3 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -32,19 +32,19 @@ export default defineConfig({ // Additional tests (commented out) // typescript-eslint - additional rules - // './tests/typescript-eslint/rules/ban-ts-comment.test.ts', - // './tests/typescript-eslint/rules/ban-tslint-comment.test.ts', - // './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this-core.test.ts', - // './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this.test.ts', - // './tests/typescript-eslint/rules/consistent-generic-constructors.test.ts', - // './tests/typescript-eslint/rules/consistent-indexed-object-style.test.ts', - // './tests/typescript-eslint/rules/consistent-return.test.ts', + './tests/typescript-eslint/rules/ban-ts-comment.test.ts', + './tests/typescript-eslint/rules/ban-tslint-comment.test.ts', + './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this-core.test.ts', + './tests/typescript-eslint/rules/class-methods-use-this/class-methods-use-this.test.ts', + './tests/typescript-eslint/rules/consistent-generic-constructors.test.ts', + './tests/typescript-eslint/rules/consistent-indexed-object-style.test.ts', + './tests/typescript-eslint/rules/consistent-return.test.ts', // './tests/typescript-eslint/rules/consistent-type-assertions.test.ts', // './tests/typescript-eslint/rules/consistent-type-definitions.test.ts', - // './tests/typescript-eslint/rules/consistent-type-exports.test.ts', + './tests/typescript-eslint/rules/consistent-type-exports.test.ts', // './tests/typescript-eslint/rules/consistent-type-imports.test.ts', - // './tests/typescript-eslint/rules/default-param-last.test.ts', - // './tests/typescript-eslint/rules/explicit-function-return-type.test.ts', + './tests/typescript-eslint/rules/default-param-last.test.ts', + './tests/typescript-eslint/rules/explicit-function-return-type.test.ts', // './tests/typescript-eslint/rules/explicit-member-accessibility.test.ts', // './tests/typescript-eslint/rules/explicit-module-boundary-types.test.ts', // './tests/typescript-eslint/rules/init-declarations.test.ts', diff --git a/packages/rslint-test-tools/tests/typescript-eslint/rules/class-methods-use-this/__snapshots__/class-methods-use-this.test.ts.snap b/packages/rslint-test-tools/tests/typescript-eslint/rules/class-methods-use-this/__snapshots__/class-methods-use-this.test.ts.snap new file mode 100644 index 000000000..0176b0243 --- /dev/null +++ b/packages/rslint-test-tools/tests/typescript-eslint/rules/class-methods-use-this/__snapshots__/class-methods-use-this.test.ts.snap @@ -0,0 +1,1205 @@ +// Rstest Snapshot v1 + +exports[`class-methods-use-this > invalid 1`] = ` +{ + "code": " +class Foo { + method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 14, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 2`] = ` +{ + "code": " +class Foo { + private method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 22, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 3`] = ` +{ + "code": " +class Foo { + protected method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 24, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 4`] = ` +{ + "code": " +class Foo { + accessor method = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 29, + "line": 3, + }, + "start": { + "column": 21, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 5`] = ` +{ + "code": " +class Foo { + private accessor method = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 37, + "line": 3, + }, + "start": { + "column": 29, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 6`] = ` +{ + "code": " +class Foo { + protected accessor method = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 39, + "line": 3, + }, + "start": { + "column": 31, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 7`] = ` +{ + "code": " +class Foo { + #method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private method #method.", + "messageId": "missingThis", + "range": { + "end": { + "column": 15, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 8`] = ` +{ + "code": " +class Foo { + get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 26, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 9`] = ` +{ + "code": " +class Foo { + private get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 34, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 10`] = ` +{ + "code": " +class Foo { + protected get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 36, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 11`] = ` +{ + "code": " +class Foo { + get #getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private getter #getter.", + "messageId": "missingThis", + "range": { + "end": { + "column": 27, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 12`] = ` +{ + "code": " +class Foo { + set setter(b: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 27, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 13`] = ` +{ + "code": " +class Foo { + private set setter(b: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 35, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 14`] = ` +{ + "code": " +class Foo { + protected set setter(b: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 37, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 15`] = ` +{ + "code": " +class Foo { + set #setter(b: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private setter #setter.", + "messageId": "missingThis", + "range": { + "end": { + "column": 28, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 16`] = ` +{ + "code": " +class Foo implements Bar { + method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 14, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 17`] = ` +{ + "code": " +class Foo implements Bar { + #method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private method #method.", + "messageId": "missingThis", + "range": { + "end": { + "column": 15, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 18`] = ` +{ + "code": " +class Foo implements Bar { + private method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 22, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 19`] = ` +{ + "code": " +class Foo implements Bar { + protected method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 24, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 20`] = ` +{ + "code": " +class Foo implements Bar { + get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 26, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 21`] = ` +{ + "code": " +class Foo implements Bar { + get #getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private getter #getter.", + "messageId": "missingThis", + "range": { + "end": { + "column": 27, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 22`] = ` +{ + "code": " +class Foo implements Bar { + private get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 34, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 23`] = ` +{ + "code": " +class Foo implements Bar { + protected get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 36, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 24`] = ` +{ + "code": " +class Foo implements Bar { + set setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 27, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 25`] = ` +{ + "code": " +class Foo implements Bar { + set #setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private setter #setter.", + "messageId": "missingThis", + "range": { + "end": { + "column": 28, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 26`] = ` +{ + "code": " +class Foo implements Bar { + private set setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 35, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 27`] = ` +{ + "code": " +class Foo implements Bar { + protected set setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 37, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 28`] = ` +{ + "code": " +class Foo { + override method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 23, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 29`] = ` +{ + "code": " +class Foo { + override get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 35, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 30`] = ` +{ + "code": " +class Foo { + override set setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 36, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 31`] = ` +{ + "code": " +class Foo implements Bar { + override method() {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 23, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 32`] = ` +{ + "code": " +class Foo implements Bar { + override get getter(): number {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class getter 'getter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 35, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 33`] = ` +{ + "code": " +class Foo implements Bar { + override set setter(v: number) {} +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class setter 'setter'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 36, + "line": 3, + }, + "start": { + "column": 3, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 34`] = ` +{ + "code": " +class Foo implements Bar { + property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'property'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 22, + "line": 3, + }, + "start": { + "column": 14, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 35`] = ` +{ + "code": " +class Foo implements Bar { + #property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class private method #property.", + "messageId": "missingThis", + "range": { + "end": { + "column": 23, + "line": 3, + }, + "start": { + "column": 15, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 36`] = ` +{ + "code": " +class Foo { + override property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'property'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 31, + "line": 3, + }, + "start": { + "column": 23, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 37`] = ` +{ + "code": " +class Foo implements Bar { + override property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'property'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 31, + "line": 3, + }, + "start": { + "column": 23, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 38`] = ` +{ + "code": " +class Foo implements Bar { + private property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'property'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 30, + "line": 3, + }, + "start": { + "column": 22, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 39`] = ` +{ + "code": " +class Foo implements Bar { + protected property = () => {}; +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'property'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 32, + "line": 3, + }, + "start": { + "column": 24, + "line": 3, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`; + +exports[`class-methods-use-this > invalid 40`] = ` +{ + "code": " +function fn() { + this.foo = 303; + + class Foo { + method() {} + } +} + ", + "diagnostics": [ + { + "message": "Expected 'this' to be used by class method 'method'.", + "messageId": "missingThis", + "range": { + "end": { + "column": 16, + "line": 6, + }, + "start": { + "column": 5, + "line": 6, + }, + }, + "ruleName": "@typescript-eslint/class-methods-use-this", + }, + ], + "errorCount": 1, + "fileCount": 1, + "ruleCount": 1, +} +`;