no_empty_interface

package
v0.1.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2025 License: MIT Imports: 5 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var NoEmptyInterfaceRule = rule.CreateRule(rule.Rule{
	Name: "no-empty-interface",
	Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
		opts := NoEmptyInterfaceOptions{
			AllowSingleExtends: false,
		}

		if options != nil {
			var optsMap map[string]interface{}
			var ok bool

			if optArray, isArray := options.([]interface{}); isArray && len(optArray) > 0 {
				optsMap, ok = optArray[0].(map[string]interface{})
			} else {

				optsMap, ok = options.(map[string]interface{})
			}

			if ok {
				if allowSingleExtends, ok := optsMap["allowSingleExtends"].(bool); ok {
					opts.AllowSingleExtends = allowSingleExtends
				}
			}
		}

		return rule.RuleListeners{
			ast.KindInterfaceDeclaration: func(node *ast.Node) {
				interfaceDecl := node.AsInterfaceDeclaration()
				if interfaceDecl == nil {
					return
				}

				if interfaceDecl.Members != nil && len(interfaceDecl.Members.Nodes) > 0 {

					return
				}

				extendCount := 0
				var extendClause *ast.HeritageClause
				if interfaceDecl.HeritageClauses != nil {
					for _, clause := range interfaceDecl.HeritageClauses.Nodes {
						heritageClause := clause.AsHeritageClause()
						if heritageClause == nil {
							continue
						}
						if heritageClause.Token == ast.KindExtendsKeyword {
							extendClause = heritageClause
							extendCount = len(heritageClause.Types.Nodes)
							break
						}
					}
				}

				if extendCount == 0 {
					ctx.ReportNode(interfaceDecl.Name(), rule.RuleMessage{
						Id:          "noEmpty",
						Description: "An empty interface is equivalent to `{}`.",
					})
					return
				}

				if extendCount == 1 && !opts.AllowSingleExtends {

					mergedWithClassDeclaration := false
					if ctx.TypeChecker != nil {
						symbol := ctx.TypeChecker.GetSymbolAtLocation(interfaceDecl.Name())
						if symbol != nil {

							for _, decl := range symbol.Declarations {
								if decl.Kind == ast.KindClassDeclaration {
									mergedWithClassDeclaration = true
									break
								}
							}
						}
					}

					isInAmbientDeclaration := false
					if strings.HasSuffix(ctx.SourceFile.FileName(), ".d.ts") {

						parent := node.Parent
						for parent != nil {
							if parent.Kind == ast.KindModuleDeclaration {
								moduleDecl := parent.AsModuleDeclaration()
								if moduleDecl == nil {
									parent = parent.Parent
									continue
								}
								modifiers := moduleDecl.Modifiers()
								if modifiers != nil {
									for _, modifier := range modifiers.Nodes {
										if modifier.Kind == ast.KindDeclareKeyword {
											isInAmbientDeclaration = true
											break
										}
									}
								}
							}
							if isInAmbientDeclaration {
								break
							}
							parent = parent.Parent
						}
					}

					// Build the replacement text
					// Check for export modifier
					var exportText string
					if interfaceDecl.Modifiers() != nil {
						for _, modifier := range interfaceDecl.Modifiers().Nodes {
							if modifier.Kind == ast.KindExportKeyword {
								exportText = "export "
								break
							}
						}
					}

					nameRange := utils.TrimNodeTextRange(ctx.SourceFile, interfaceDecl.Name())
					nameText := ctx.SourceFile.Text()[nameRange.Pos():nameRange.End()]

					// Extract type parameters if present
					var typeParamsText string
					if interfaceDecl.TypeParameters != nil && len(interfaceDecl.TypeParameters.Nodes) > 0 {

						firstParam := interfaceDecl.TypeParameters.Nodes[0]
						lastParam := interfaceDecl.TypeParameters.Nodes[len(interfaceDecl.TypeParameters.Nodes)-1]
						firstRange := utils.TrimNodeTextRange(ctx.SourceFile, firstParam)
						lastRange := utils.TrimNodeTextRange(ctx.SourceFile, lastParam)
						typeParamsRange := firstRange.WithEnd(lastRange.End())

						typeParamsRange = typeParamsRange.WithPos(typeParamsRange.Pos() - 1).WithEnd(typeParamsRange.End() + 1)
						typeParamsText = ctx.SourceFile.Text()[typeParamsRange.Pos():typeParamsRange.End()]
					}

					extendedTypeRange := utils.TrimNodeTextRange(ctx.SourceFile, extendClause.Types.Nodes[0])
					extendedTypeText := ctx.SourceFile.Text()[extendedTypeRange.Pos():extendedTypeRange.End()]

					replacement := fmt.Sprintf("%stype %s%s = %s", exportText, nameText, typeParamsText, extendedTypeText)

					message := rule.RuleMessage{
						Id:          "noEmptyWithSuper",
						Description: "An interface declaring no members is equivalent to its supertype.",
					}

					if isInAmbientDeclaration || mergedWithClassDeclaration {

						ctx.ReportNode(interfaceDecl.Name(), message)
					} else {

						ctx.ReportNodeWithFixes(interfaceDecl.Name(), message,
							rule.RuleFixReplace(ctx.SourceFile, node, replacement))
					}
				}
			},
		}
	},
})

Functions

This section is empty.

Types

type NoEmptyInterfaceOptions

type NoEmptyInterfaceOptions struct {
	AllowSingleExtends bool `json:"allowSingleExtends"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL