mirror of
				https://github.com/mgechev/revive.git
				synced 2025-10-30 23:37:49 +02:00 
			
		
		
		
	Allow revive to be called with extra linters (#650)
This change allows revive to be called from main.go in other libraries and pass in a list of custom linters to be added to the built-in linters found in config Co-authored-by: Bernardo Heynemann <bernardo.heynemann@coinbase.com> Co-authored-by: chavacava <salvadorcavadini+github@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							5ce2ff53c0
						
					
				
				
					commit
					1c283837a9
				
			
							
								
								
									
										281
									
								
								cli/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								cli/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,281 @@ | ||||
| package cli | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/fatih/color" | ||||
| 	"github.com/mgechev/dots" | ||||
| 	"github.com/mgechev/revive/config" | ||||
| 	"github.com/mgechev/revive/lint" | ||||
| 	"github.com/mgechev/revive/logging" | ||||
| 	"github.com/mitchellh/go-homedir" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	version = "dev" | ||||
| 	commit  = "none" | ||||
| 	date    = "unknown" | ||||
| 	builtBy = "unknown" | ||||
| ) | ||||
|  | ||||
| func fail(err string) { | ||||
| 	fmt.Fprintln(os.Stderr, err) | ||||
| 	os.Exit(1) | ||||
| } | ||||
|  | ||||
| // ExtraRule configures a new rule to be used with revive. | ||||
| type ExtraRule struct { | ||||
| 	Rule          lint.Rule | ||||
| 	DefaultConfig lint.RuleConfig | ||||
| } | ||||
|  | ||||
| // NewExtraRule returns a configured extra rule | ||||
| func NewExtraRule(rule lint.Rule, defaultConfig lint.RuleConfig) ExtraRule { | ||||
| 	return ExtraRule{ | ||||
| 		Rule:          rule, | ||||
| 		DefaultConfig: defaultConfig, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // RunRevive runs the CLI for revive. | ||||
| func RunRevive(extraRules ...ExtraRule) { | ||||
| 	log, err := logging.GetLogger() | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	formatter, err := config.GetFormatter(formatterName) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	conf, err := config.GetConfig(configPath) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if setExitStatus { | ||||
| 		conf.ErrorCode = 1 | ||||
| 		conf.WarningCode = 1 | ||||
| 	} | ||||
|  | ||||
| 	extraRuleInstances := make([]lint.Rule, len(extraRules)) | ||||
| 	for i, extraRule := range extraRules { | ||||
| 		extraRuleInstances[i] = extraRule.Rule | ||||
|  | ||||
| 		ruleName := extraRule.Rule.Name() | ||||
| 		_, isRuleAlreadyConfigured := conf.Rules[ruleName] | ||||
| 		if !isRuleAlreadyConfigured { | ||||
| 			conf.Rules[ruleName] = extraRule.DefaultConfig | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	lintingRules, err := config.GetLintingRules(conf, extraRuleInstances) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	log.Println("Config loaded") | ||||
|  | ||||
| 	if len(excludePaths) == 0 { // if no excludes were set in the command line | ||||
| 		excludePaths = conf.Exclude // use those from the configuration | ||||
| 	} | ||||
|  | ||||
| 	packages, err := getPackages(excludePaths) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
| 	revive := lint.New(func(file string) ([]byte, error) { | ||||
| 		return ioutil.ReadFile(file) | ||||
| 	}, maxOpenFiles) | ||||
|  | ||||
| 	failures, err := revive.Lint(packages, lintingRules, *conf) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	formatChan := make(chan lint.Failure) | ||||
| 	exitChan := make(chan bool) | ||||
|  | ||||
| 	var output string | ||||
| 	go (func() { | ||||
| 		output, err = formatter.Format(formatChan, *conf) | ||||
| 		if err != nil { | ||||
| 			fail(err.Error()) | ||||
| 		} | ||||
| 		exitChan <- true | ||||
| 	})() | ||||
|  | ||||
| 	exitCode := 0 | ||||
| 	for f := range failures { | ||||
| 		if f.Confidence < conf.Confidence { | ||||
| 			continue | ||||
| 		} | ||||
| 		if exitCode == 0 { | ||||
| 			exitCode = conf.WarningCode | ||||
| 		} | ||||
| 		if c, ok := conf.Rules[f.RuleName]; ok && c.Severity == lint.SeverityError { | ||||
| 			exitCode = conf.ErrorCode | ||||
| 		} | ||||
| 		if c, ok := conf.Directives[f.RuleName]; ok && c.Severity == lint.SeverityError { | ||||
| 			exitCode = conf.ErrorCode | ||||
| 		} | ||||
|  | ||||
| 		formatChan <- f | ||||
| 	} | ||||
|  | ||||
| 	close(formatChan) | ||||
| 	<-exitChan | ||||
| 	if output != "" { | ||||
| 		fmt.Println(output) | ||||
| 	} | ||||
|  | ||||
| 	os.Exit(exitCode) | ||||
| } | ||||
|  | ||||
| func normalizeSplit(strs []string) []string { | ||||
| 	res := []string{} | ||||
| 	for _, s := range strs { | ||||
| 		t := strings.Trim(s, " \t") | ||||
| 		if len(t) > 0 { | ||||
| 			res = append(res, t) | ||||
| 		} | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func getPackages(excludePaths arrayFlags) ([][]string, error) { | ||||
| 	globs := normalizeSplit(flag.Args()) | ||||
| 	if len(globs) == 0 { | ||||
| 		globs = append(globs, ".") | ||||
| 	} | ||||
|  | ||||
| 	packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePaths)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return packages, nil | ||||
| } | ||||
|  | ||||
| type arrayFlags []string | ||||
|  | ||||
| func (i *arrayFlags) String() string { | ||||
| 	return strings.Join([]string(*i), " ") | ||||
| } | ||||
|  | ||||
| func (i *arrayFlags) Set(value string) error { | ||||
| 	*i = append(*i, value) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	configPath    string | ||||
| 	excludePaths  arrayFlags | ||||
| 	formatterName string | ||||
| 	help          bool | ||||
| 	versionFlag   bool | ||||
| 	setExitStatus bool | ||||
| 	maxOpenFiles  int | ||||
| ) | ||||
|  | ||||
| var originalUsage = flag.Usage | ||||
|  | ||||
| func getLogo() string { | ||||
| 	return color.YellowString(` _ __ _____   _(_)__  _____ | ||||
| | '__/ _ \ \ / / \ \ / / _ \ | ||||
| | | |  __/\ V /| |\ V /  __/ | ||||
| |_|  \___| \_/ |_| \_/ \___|`) | ||||
| } | ||||
|  | ||||
| func getCall() string { | ||||
| 	return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...") | ||||
| } | ||||
|  | ||||
| func getBanner() string { | ||||
| 	return fmt.Sprintf(` | ||||
| %s | ||||
|  | ||||
| Example: | ||||
|   %s | ||||
| `, getLogo(), getCall()) | ||||
| } | ||||
|  | ||||
| func buildDefaultConfigPath() string { | ||||
| 	var result string | ||||
| 	if homeDir, err := homedir.Dir(); err == nil { | ||||
| 		result = filepath.Join(homeDir, "revive.toml") | ||||
| 		if _, err := os.Stat(result); err != nil { | ||||
| 			result = "" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	// Force colorizing for no TTY environments | ||||
| 	if os.Getenv("REVIVE_FORCE_COLOR") == "1" { | ||||
| 		color.NoColor = false | ||||
| 	} | ||||
|  | ||||
| 	flag.Usage = func() { | ||||
| 		fmt.Println(getBanner()) | ||||
| 		originalUsage() | ||||
| 	} | ||||
|  | ||||
| 	// command line help strings | ||||
| 	const ( | ||||
| 		configUsage       = "path to the configuration TOML file, defaults to $HOME/revive.toml, if present (i.e. -config myconf.toml)" | ||||
| 		excludeUsage      = "list of globs which specify files to be excluded (i.e. -exclude foo/...)" | ||||
| 		formatterUsage    = "formatter to be used for the output (i.e. -formatter stylish)" | ||||
| 		versionUsage      = "get revive version" | ||||
| 		exitStatusUsage   = "set exit status to 1 if any issues are found, overwrites errorCode and warningCode in config" | ||||
| 		maxOpenFilesUsage = "maximum number of open files at the same time" | ||||
| 	) | ||||
|  | ||||
| 	defaultConfigPath := buildDefaultConfigPath() | ||||
|  | ||||
| 	flag.StringVar(&configPath, "config", defaultConfigPath, configUsage) | ||||
| 	flag.Var(&excludePaths, "exclude", excludeUsage) | ||||
| 	flag.StringVar(&formatterName, "formatter", "", formatterUsage) | ||||
| 	flag.BoolVar(&versionFlag, "version", false, versionUsage) | ||||
| 	flag.BoolVar(&setExitStatus, "set_exit_status", false, exitStatusUsage) | ||||
| 	flag.IntVar(&maxOpenFiles, "max_open_files", 0, maxOpenFilesUsage) | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	// Output build info (version, commit, date and builtBy) | ||||
| 	if versionFlag { | ||||
| 		var buildInfo string | ||||
| 		if date != "unknown" && builtBy != "unknown" { | ||||
| 			buildInfo = fmt.Sprintf("Built\t\t%s by %s\n", date, builtBy) | ||||
| 		} | ||||
|  | ||||
| 		if commit != "none" { | ||||
| 			buildInfo = fmt.Sprintf("Commit:\t\t%s\n%s", commit, buildInfo) | ||||
| 		} | ||||
|  | ||||
| 		if version == "dev" { | ||||
| 			bi, ok := debug.ReadBuildInfo() | ||||
| 			if ok { | ||||
| 				version = bi.Main.Version | ||||
| 				if strings.HasPrefix(version, "v") { | ||||
| 					version = bi.Main.Version[1:] | ||||
| 				} | ||||
| 				if len(buildInfo) == 0 { | ||||
| 					fmt.Printf("version %s\n", version) | ||||
| 					os.Exit(0) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		fmt.Printf("Version:\t%s\n%s", version, buildInfo) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| } | ||||
| @@ -107,11 +107,17 @@ func getFormatters() map[string]lint.Formatter { | ||||
| } | ||||
|  | ||||
| // GetLintingRules yields the linting rules that must be applied by the linter | ||||
| func GetLintingRules(config *lint.Config) ([]lint.Rule, error) { | ||||
| func GetLintingRules(config *lint.Config, extraRules []lint.Rule) ([]lint.Rule, error) { | ||||
| 	rulesMap := map[string]lint.Rule{} | ||||
| 	for _, r := range allRules { | ||||
| 		rulesMap[r.Name()] = r | ||||
| 	} | ||||
| 	for _, r := range extraRules { | ||||
| 		if _, ok := rulesMap[r.Name()]; ok { | ||||
| 			continue | ||||
| 		} | ||||
| 		rulesMap[r.Name()] = r | ||||
| 	} | ||||
|  | ||||
| 	var lintingRules []lint.Rule | ||||
| 	for name, ruleConfig := range config.Rules { | ||||
|   | ||||
| @@ -92,7 +92,7 @@ func TestGetLintingRules(t *testing.T) { | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error while loading conf: %v", err) | ||||
| 			} | ||||
| 			rules, err := GetLintingRules(cfg) | ||||
| 			rules, err := GetLintingRules(cfg, []lint.Rule{}) | ||||
| 			switch { | ||||
| 			case err != nil: | ||||
| 				t.Fatalf("Unexpected error\n\t%v", err) | ||||
| @@ -130,7 +130,7 @@ func TestGetGlobalSeverity(t *testing.T) { | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error while loading conf: %v", err) | ||||
| 			} | ||||
| 			rules, err := GetLintingRules(cfg) | ||||
| 			rules, err := GetLintingRules(cfg, []lint.Rule{}) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error while loading conf: %v", err) | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										251
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										251
									
								
								main.go
									
									
									
									
									
								
							| @@ -1,254 +1,7 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime/debug" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/fatih/color" | ||||
| 	"github.com/mgechev/dots" | ||||
| 	"github.com/mgechev/revive/config" | ||||
| 	"github.com/mgechev/revive/lint" | ||||
| 	"github.com/mgechev/revive/logging" | ||||
| 	"github.com/mitchellh/go-homedir" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	version = "dev" | ||||
| 	commit  = "none" | ||||
| 	date    = "unknown" | ||||
| 	builtBy = "unknown" | ||||
| ) | ||||
|  | ||||
| func fail(err string) { | ||||
| 	fmt.Fprintln(os.Stderr, err) | ||||
| 	os.Exit(1) | ||||
| } | ||||
| import "github.com/mgechev/revive/cli" | ||||
|  | ||||
| func main() { | ||||
| 	log, err := logging.GetLogger() | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	conf, err := config.GetConfig(configPath) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
| 	formatter, err := config.GetFormatter(formatterName) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
| 	if setExitStatus { | ||||
| 		conf.ErrorCode = 1 | ||||
| 		conf.WarningCode = 1 | ||||
| 	} | ||||
|  | ||||
| 	if len(excludePaths) == 0 { // if no excludes were set in the command line | ||||
| 		excludePaths = conf.Exclude // use those from the configuration | ||||
| 	} | ||||
|  | ||||
| 	packages, err := getPackages(excludePaths) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	revive := lint.New(func(file string) ([]byte, error) { | ||||
| 		return ioutil.ReadFile(file) | ||||
| 	}, maxOpenFiles) | ||||
|  | ||||
| 	lintingRules, err := config.GetLintingRules(conf) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	log.Println("Config loaded") | ||||
|  | ||||
| 	failures, err := revive.Lint(packages, lintingRules, *conf) | ||||
| 	if err != nil { | ||||
| 		fail(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	formatChan := make(chan lint.Failure) | ||||
| 	exitChan := make(chan bool) | ||||
|  | ||||
| 	var output string | ||||
| 	go (func() { | ||||
| 		output, err = formatter.Format(formatChan, *conf) | ||||
| 		if err != nil { | ||||
| 			fail(err.Error()) | ||||
| 		} | ||||
| 		exitChan <- true | ||||
| 	})() | ||||
|  | ||||
| 	exitCode := 0 | ||||
| 	for f := range failures { | ||||
| 		if f.Confidence < conf.Confidence { | ||||
| 			continue | ||||
| 		} | ||||
| 		if exitCode == 0 { | ||||
| 			exitCode = conf.WarningCode | ||||
| 		} | ||||
| 		if c, ok := conf.Rules[f.RuleName]; ok && c.Severity == lint.SeverityError { | ||||
| 			exitCode = conf.ErrorCode | ||||
| 		} | ||||
| 		if c, ok := conf.Directives[f.RuleName]; ok && c.Severity == lint.SeverityError { | ||||
| 			exitCode = conf.ErrorCode | ||||
| 		} | ||||
|  | ||||
| 		formatChan <- f | ||||
| 	} | ||||
|  | ||||
| 	close(formatChan) | ||||
| 	<-exitChan | ||||
| 	if output != "" { | ||||
| 		fmt.Println(output) | ||||
| 	} | ||||
|  | ||||
| 	os.Exit(exitCode) | ||||
| } | ||||
|  | ||||
| func normalizeSplit(strs []string) []string { | ||||
| 	res := []string{} | ||||
| 	for _, s := range strs { | ||||
| 		t := strings.Trim(s, " \t") | ||||
| 		if len(t) > 0 { | ||||
| 			res = append(res, t) | ||||
| 		} | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func getPackages(excludePaths arrayFlags) ([][]string, error) { | ||||
| 	globs := normalizeSplit(flag.Args()) | ||||
| 	if len(globs) == 0 { | ||||
| 		globs = append(globs, ".") | ||||
| 	} | ||||
|  | ||||
| 	packages, err := dots.ResolvePackages(globs, normalizeSplit(excludePaths)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return packages, nil | ||||
| } | ||||
|  | ||||
| type arrayFlags []string | ||||
|  | ||||
| func (i *arrayFlags) String() string { | ||||
| 	return strings.Join([]string(*i), " ") | ||||
| } | ||||
|  | ||||
| func (i *arrayFlags) Set(value string) error { | ||||
| 	*i = append(*i, value) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	configPath    string | ||||
| 	excludePaths  arrayFlags | ||||
| 	formatterName string | ||||
| 	help          bool | ||||
| 	versionFlag   bool | ||||
| 	setExitStatus bool | ||||
| 	maxOpenFiles  int | ||||
| ) | ||||
|  | ||||
| var originalUsage = flag.Usage | ||||
|  | ||||
| func getLogo() string { | ||||
| 	return color.YellowString(` _ __ _____   _(_)__  _____ | ||||
| | '__/ _ \ \ / / \ \ / / _ \ | ||||
| | | |  __/\ V /| |\ V /  __/ | ||||
| |_|  \___| \_/ |_| \_/ \___|`) | ||||
| } | ||||
|  | ||||
| func getCall() string { | ||||
| 	return color.MagentaString("revive -config c.toml -formatter friendly -exclude a.go -exclude b.go ./...") | ||||
| } | ||||
|  | ||||
| func getBanner() string { | ||||
| 	return fmt.Sprintf(` | ||||
| %s | ||||
|  | ||||
| Example: | ||||
|   %s | ||||
| `, getLogo(), getCall()) | ||||
| } | ||||
|  | ||||
| func buildDefaultConfigPath() string { | ||||
| 	var result string | ||||
| 	if homeDir, err := homedir.Dir(); err == nil { | ||||
| 		result = filepath.Join(homeDir, "revive.toml") | ||||
| 		if _, err := os.Stat(result); err != nil { | ||||
| 			result = "" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	// Force colorizing for no TTY environments | ||||
| 	if os.Getenv("REVIVE_FORCE_COLOR") == "1" { | ||||
| 		color.NoColor = false | ||||
| 	} | ||||
|  | ||||
| 	flag.Usage = func() { | ||||
| 		fmt.Println(getBanner()) | ||||
| 		originalUsage() | ||||
| 	} | ||||
|  | ||||
| 	// command line help strings | ||||
| 	const ( | ||||
| 		configUsage       = "path to the configuration TOML file, defaults to $HOME/revive.toml, if present (i.e. -config myconf.toml)" | ||||
| 		excludeUsage      = "list of globs which specify files to be excluded (i.e. -exclude foo/...)" | ||||
| 		formatterUsage    = "formatter to be used for the output (i.e. -formatter stylish)" | ||||
| 		versionUsage      = "get revive version" | ||||
| 		exitStatusUsage   = "set exit status to 1 if any issues are found, overwrites errorCode and warningCode in config" | ||||
| 		maxOpenFilesUsage = "maximum number of open files at the same time" | ||||
| 	) | ||||
|  | ||||
| 	defaultConfigPath := buildDefaultConfigPath() | ||||
|  | ||||
| 	flag.StringVar(&configPath, "config", defaultConfigPath, configUsage) | ||||
| 	flag.Var(&excludePaths, "exclude", excludeUsage) | ||||
| 	flag.StringVar(&formatterName, "formatter", "", formatterUsage) | ||||
| 	flag.BoolVar(&versionFlag, "version", false, versionUsage) | ||||
| 	flag.BoolVar(&setExitStatus, "set_exit_status", false, exitStatusUsage) | ||||
| 	flag.IntVar(&maxOpenFiles, "max_open_files", 0, maxOpenFilesUsage) | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	// Output build info (version, commit, date and builtBy) | ||||
| 	if versionFlag { | ||||
| 		var buildInfo string | ||||
| 		if date != "unknown" && builtBy != "unknown" { | ||||
| 			buildInfo = fmt.Sprintf("Built\t\t%s by %s\n", date, builtBy) | ||||
| 		} | ||||
|  | ||||
| 		if commit != "none" { | ||||
| 			buildInfo = fmt.Sprintf("Commit:\t\t%s\n%s", commit, buildInfo) | ||||
| 		} | ||||
|  | ||||
| 		if version == "dev" { | ||||
| 			bi, ok := debug.ReadBuildInfo() | ||||
| 			if ok { | ||||
| 				version = bi.Main.Version | ||||
| 				if strings.HasPrefix(version, "v") { | ||||
| 					version = bi.Main.Version[1:] | ||||
| 				} | ||||
| 				if len(buildInfo) == 0 { | ||||
| 					fmt.Printf("version %s\n", version) | ||||
| 					os.Exit(0) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		fmt.Printf("Version:\t%s\n%s", version, buildInfo) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 	cli.RunRevive() | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user