From 6bed57daab7ccd404ffbfe2d3cfb424cdf575705 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:13:31 +0200 Subject: [PATCH 1/7] feat(rust_style): copy `camelcase` --- docs/rules/rust_style.md | 73 +++ src/rules/rust_style.rs | 1250 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1323 insertions(+) create mode 100644 docs/rules/rust_style.md create mode 100644 src/rules/rust_style.rs diff --git a/docs/rules/rust_style.md b/docs/rules/rust_style.md new file mode 100644 index 000000000..f4cd0ce12 --- /dev/null +++ b/docs/rules/rust_style.md @@ -0,0 +1,73 @@ +Enforces the use of camelCase in variable names + +Consistency in a code base is key for readability and maintainability. This rule +enforces variable declarations and object property names which you create to be +in camelCase. + +Of note: + +- `_` is allowed at the start or end of a variable +- All uppercase variable names (e.g. constants) may have `_` in their name +- If you have to use a snake_case key in an object for some reasons, wrap it in + quotation mark +- This rule also applies to variables imported or exported via ES modules, but + not to object properties of those variables + +### Invalid: + +```typescript +let first_name = "Ichigo"; +const obj1 = { last_name: "Hoshimiya" }; +const obj2 = { first_name }; +const { last_name } = obj1; + +function do_something() {} +function foo({ snake_case = "default value" }) {} + +class snake_case_class {} +class Also_Not_Valid_Class {} + +import { not_camelCased } from "external-module.js"; +export * as not_camelCased from "mod.ts"; + +enum snake_case_enum { + snake_case_variant, +} + +type snake_case_type = { some_property: number }; + +interface snake_case_interface { + some_property: number; +} +``` + +### Valid: + +```typescript +let firstName = "Ichigo"; +const FIRST_NAME = "Ichigo"; +const __myPrivateVariable = "Hoshimiya"; +const myPrivateVariable_ = "Hoshimiya"; +const obj1 = { "last_name": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid +const obj2 = { "first_name": first_name }; +const { last_name: lastName } = obj; + +function doSomething() {} // function declarations must be camelCase but... +do_something(); // ...snake_case function calls are allowed +function foo({ snake_case: camelCase = "default value" }) {} + +class PascalCaseClass {} + +import { not_camelCased as camelCased } from "external-module.js"; +export * as camelCased from "mod.ts"; + +enum PascalCaseEnum { + PascalCaseVariant, +} + +type PascalCaseType = { someProperty: number }; + +interface PascalCaseInterface { + someProperty: number; +} +``` diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs new file mode 100644 index 000000000..e45184491 --- /dev/null +++ b/src/rules/rust_style.rs @@ -0,0 +1,1250 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::{Context, LintRule}; +use crate::handler::{Handler, Traverse}; +use crate::swc_util::StringRepr; + +use deno_ast::view::{Node, NodeKind, NodeTrait}; +use deno_ast::{view as ast_view, SourceRange, SourceRanged}; +use once_cell::sync::Lazy; +use regex::{Captures, Regex}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug)] +pub struct Camelcase; + +const CODE: &str = "camelcase"; + +impl LintRule for Camelcase { + fn tags(&self) -> &'static [&'static str] { + &[] + } + + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view( + &self, + context: &mut Context, + program: ast_view::Program, + ) { + let mut handler = CamelcaseHandler::default(); + handler.traverse(program, context); + handler.report_errors(context); + } + + #[cfg(feature = "docs")] + fn docs(&self) -> &'static str { + include_str!("../../docs/rules/camelcase.md") + } +} + +/// Check if it contains underscores, except for leading and trailing ones +fn is_underscored(ident_name: &str) -> bool { + let trimmed_ident = ident_name.trim_matches('_'); + trimmed_ident.contains('_') + && trimmed_ident != trimmed_ident.to_ascii_uppercase() +} + +/// Convert the name of identifier into camel case. If the name is originally in camel case, return +/// the name as it is. For more detail, see the test cases below. +fn to_camelcase(ident_name: &str) -> String { + if !is_underscored(ident_name) { + return ident_name.to_string(); + } + + static UNDERSCORE_CHAR_RE: Lazy = + Lazy::new(|| Regex::new(r"([^_])_([a-z])").unwrap()); + + let result = UNDERSCORE_CHAR_RE.replace_all(ident_name, |caps: &Captures| { + format!("{}{}", &caps[1], caps[2].to_ascii_uppercase()) + }); + + if result != ident_name { + return result.into_owned(); + } + + ident_name.to_ascii_uppercase() +} + +enum IdentToCheck { + /// Normal variable name e.g. `foo` in `const foo = 42;` + Variable(String), + /// Object key name, for example: + /// + /// ```typescript + /// const obj = { foo: 42 }; // key_name: foo, is_shorthand: false + /// + /// const obj2 = { someVariable }; // key_name: someVariable, is_shorthand: true + /// ``` + ObjectKey { + key_name: String, + is_shorthand: bool, + }, + /// Function name e.g. `foo` in `function foo() {}` + Function(String), + /// Class name e.g. `Foo` in `class Foo {}` + Class(String), + /// Type alias e.g. `Foo` in `type Foo = string;` + TypeAlias(String), + /// Interface name e.g. `Foo` in `interface Foo {}` + Interface(String), + /// Enum name e.g. `Foo` in `enum Foo {}` + EnumName(String), + /// Enum variant e.g. `Bar` in `enum Foo { Bar }` + EnumVariant(String), + /// Namespace e.g. `Foo` in `namespace Foo {}` + Namespace(String), + /// Module e.g. `Foo` in `module Foo {}` + Module(String), + /// Key and value name in object pattern, for example: + /// + /// ```typescript + /// // key_name: foo, value_name: None, has_default: false + /// const { foo } = obj1; + /// + /// // key_name: foo, value_name: Some(bar), has_default: false + /// const { foo: bar } = obj2; + /// + /// // key_name: foo, value_name: Some(bar), has_default: true + /// const { foo: bar = baz } = obj3; // key_name: foo, value_name: Some(bar), + /// + /// // key_name: foo, value_name: None, has_default: false + /// function f({ foo }) {} + /// ``` + ObjectPat { + key_name: String, + value_name: Option, + has_default: bool, + is_destructuring: bool, + }, + /// Local name and imported name in named import, for example: + /// + /// ```typescript + /// import { foo } from 'mod.ts'; // local: foo, imported: None + /// + /// import { foo as bar } from 'mod.ts'; // local: bar, imported: Some(foo) + /// ``` + NamedImport { + local: String, + imported: Option, + }, +} + +impl IdentToCheck { + fn variable(name: impl AsRef) -> Self { + Self::Variable(name.as_ref().to_string()) + } + + fn object_key(key_name: impl AsRef, is_shorthand: bool) -> Self { + Self::ObjectKey { + key_name: key_name.as_ref().to_string(), + is_shorthand, + } + } + + fn function(name: impl AsRef) -> Self { + Self::Function(name.as_ref().to_string()) + } + + fn class(name: impl AsRef) -> Self { + Self::Class(name.as_ref().to_string()) + } + + fn type_alias(name: impl AsRef) -> Self { + Self::TypeAlias(name.as_ref().to_string()) + } + + fn interface(name: impl AsRef) -> Self { + Self::Interface(name.as_ref().to_string()) + } + + fn enum_name(name: impl AsRef) -> Self { + Self::EnumName(name.as_ref().to_string()) + } + + fn enum_variant(name: impl AsRef) -> Self { + Self::EnumVariant(name.as_ref().to_string()) + } + + fn namespace(name: impl AsRef) -> Self { + Self::Namespace(name.as_ref().to_string()) + } + + fn module(name: impl AsRef) -> Self { + Self::Module(name.as_ref().to_string()) + } + + fn object_pat( + key_name: &K, + value_name: Option<&V>, + has_default: bool, + is_destructuring: bool, + ) -> Self + where + K: AsRef, + V: AsRef, + { + Self::ObjectPat { + key_name: key_name.as_ref().to_string(), + value_name: value_name.map(|v| v.as_ref().to_string()), + has_default, + is_destructuring, + } + } + + fn named_import(local: &L, imported: Option<&I>) -> Self + where + L: AsRef, + I: AsRef, + { + Self::NamedImport { + local: local.as_ref().to_string(), + imported: imported.map(|i| i.as_ref().to_string()), + } + } + + fn get_ident_name(&self) -> &str { + match self { + IdentToCheck::Variable(name) + | IdentToCheck::Function(name) + | IdentToCheck::Class(name) + | IdentToCheck::TypeAlias(name) + | IdentToCheck::Interface(name) + | IdentToCheck::EnumName(name) + | IdentToCheck::EnumVariant(name) + | IdentToCheck::Namespace(name) + | IdentToCheck::Module(name) => name, + IdentToCheck::ObjectKey { ref key_name, .. } => key_name, + IdentToCheck::ObjectPat { + key_name, + value_name, + .. + } => { + if let Some(value_name) = value_name { + value_name + } else { + key_name + } + } + IdentToCheck::NamedImport { local, .. } => local, + } + } + + fn to_message(&self) -> String { + format!( + "Identifier '{}' is not in camel case.", + self.get_ident_name() + ) + } + + fn to_hint(&self) -> String { + match self { + IdentToCheck::Variable(name) | IdentToCheck::Function(name) => { + format!("Consider renaming `{}` to `{}`", name, to_camelcase(name)) + } + IdentToCheck::ObjectKey { + ref key_name, + is_shorthand, + } => { + if *is_shorthand { + format!( + r#"Consider writing `{camel_cased}: {original}` or `"{original}": {original}`"#, + camel_cased = to_camelcase(key_name), + original = key_name + ) + } else { + format!( + r#"Consider renaming `{original}` to `{camel_cased}`, or wrapping it in quotation mark like `"{original}"`"#, + camel_cased = to_camelcase(key_name), + original = key_name + ) + } + } + IdentToCheck::Class(name) + | IdentToCheck::TypeAlias(name) + | IdentToCheck::Interface(name) + | IdentToCheck::EnumName(name) + | IdentToCheck::EnumVariant(name) + | IdentToCheck::Namespace(name) + | IdentToCheck::Module(name) => { + let camel_cased = to_camelcase(name); + static FIRST_CHAR_LOWERCASE: Lazy = + Lazy::new(|| Regex::new(r"^[a-z]").unwrap()); + + // The following names should be in pascal case + // - class + // - type alias + // - interface + // - enum + // - enum variant + // - namespace + // - module + let pascal_cased = FIRST_CHAR_LOWERCASE + .replace(&camel_cased, |caps: &Captures| { + caps[0].to_ascii_uppercase() + }); + format!("Consider renaming `{}` to `{}`", name, pascal_cased) + } + IdentToCheck::ObjectPat { + key_name, + value_name, + has_default, + is_destructuring: in_var_decl, + } => { + let rename_name = if let Some(value_name) = value_name { + Some(value_name) + } else if *in_var_decl { + None + } else { + Some(key_name) + }; + if let Some(name) = rename_name { + return format!( + "Consider renaming `{}` to `{}`", + name, + to_camelcase(name), + ); + } + + if *has_default { + return format!( + "Consider replacing `{{ {key} = .. }}` with `{{ {key}: {value} = .. }}`", + key = key_name, + value = to_camelcase(key_name), + ); + } + + format!( + "Consider replacing `{{ {key} }}` with `{{ {key}: {value} }}`", + key = key_name, + value = to_camelcase(key_name), + ) + } + IdentToCheck::NamedImport { local, imported } => { + if imported.is_some() { + format!("Consider renaming `{}` to `{}`", local, to_camelcase(local)) + } else { + format!( + "Consider replacing `{{ {local} }}` with `{{ {local} as {camel_cased_local} }}`", + local = local, + camel_cased_local = to_camelcase(local), + ) + } + } + } + } +} + +#[derive(Default)] +struct CamelcaseHandler { + /// Accumulated errors to report + errors: BTreeMap, + /// Already visited identifiers + visited: BTreeSet, +} + +impl CamelcaseHandler { + /// Report accumulated errors, consuming `self`. + fn report_errors(self, ctx: &mut Context) { + for (range, error_ident) in self.errors { + ctx.add_diagnostic_with_hint( + range, + CODE, + error_ident.to_message(), + error_ident.to_hint(), + ); + } + } + + /// Check if this ident is underscored only when it's not yet visited. + fn check_ident(&mut self, range: &S, ident: IdentToCheck) { + let range = range.range(); + if self.visited.insert(range) && is_underscored(ident.get_ident_name()) { + self.errors.insert(range, ident); + } + } + + fn check_ts_type(&mut self, ty: &ast_view::TsType) { + if let ast_view::TsType::TsTypeLit(type_lit) = ty { + for member in type_lit.members { + self.check_ts_type_element(member); + } + } + } + + fn check_ts_type_element(&mut self, ty_el: &ast_view::TsTypeElement) { + use deno_ast::view::TsTypeElement::*; + match ty_el { + TsPropertySignature(prop_sig) => { + if let ast_view::Expr::Ident(ident) = prop_sig.key { + self.check_ident(ident, IdentToCheck::object_key(ident.inner, false)); + } + if let Some(type_ann) = &prop_sig.type_ann { + self.check_ts_type(&type_ann.type_ann); + } + } + TsMethodSignature(method_sig) => { + if let ast_view::Expr::Ident(ident) = method_sig.key { + self.check_ident(ident, IdentToCheck::function(ident.inner)); + } + if let Some(type_ann) = &method_sig.type_ann { + self.check_ts_type(&type_ann.type_ann); + } + } + TsGetterSignature(getter_sig) => { + if let ast_view::Expr::Ident(ident) = getter_sig.key { + self.check_ident(ident, IdentToCheck::function(ident.inner)); + } + if let Some(type_ann) = &getter_sig.type_ann { + self.check_ts_type(&type_ann.type_ann); + } + } + TsSetterSignature(setter_sig) => { + if let ast_view::Expr::Ident(ident) = setter_sig.key { + self.check_ident(ident, IdentToCheck::function(ident.inner)); + } + } + TsIndexSignature(_) + | TsCallSignatureDecl(_) + | TsConstructSignatureDecl(_) => {} + } + } + + fn check_pat(&mut self, pat: &ast_view::Pat) { + match pat { + ast_view::Pat::Ident(ident) => { + self.check_ident(ident, IdentToCheck::variable(ident.id.inner)); + } + ast_view::Pat::Array(ast_view::ArrayPat { elems, .. }) => { + for pat in elems.iter().flatten() { + self.check_pat(pat); + } + } + ast_view::Pat::Rest(ast_view::RestPat { ref arg, .. }) => { + self.check_pat(arg); + } + ast_view::Pat::Object(ast_view::ObjectPat { props, .. }) => { + for prop in *props { + match prop { + ast_view::ObjectPatProp::KeyValue(ast_view::KeyValuePatProp { + ref key, + ref value, + .. + }) => match value { + ast_view::Pat::Ident(value_ident) => { + self.check_ident( + value_ident, + IdentToCheck::object_pat( + &key.string_repr().unwrap_or_else(|| "[KEY]".to_string()), + Some(&value_ident.id.inner), + false, + pat_in_var_declarator(pat.into()), + ), + ); + } + ast_view::Pat::Assign(ast_view::AssignPat { + left: ast_view::Pat::Ident(value_ident), + .. + }) => { + self.check_ident( + value_ident, + IdentToCheck::object_pat( + &key.string_repr().unwrap_or_else(|| "[KEY]".to_string()), + Some(&value_ident.id.inner), + true, + pat_in_var_declarator(pat.into()), + ), + ); + } + _ => { + self.check_pat(value); + } + }, + ast_view::ObjectPatProp::Assign(ast_view::AssignPatProp { + ref key, + ref value, + .. + }) => { + let has_default = value.is_some(); + let in_var_declarator = pat_in_var_declarator(pat.into()); + if !in_var_declarator { + self.check_ident( + key, + IdentToCheck::object_pat::<&str, &str>( + &key.inner.as_ref(), + None, + has_default, + in_var_declarator, + ), + ); + } + } + ast_view::ObjectPatProp::Rest(ast_view::RestPat { + ref arg, + .. + }) => { + self.check_pat(arg); + } + } + } + } + ast_view::Pat::Assign(ast_view::AssignPat { ref left, .. }) => { + self.check_pat(left); + } + ast_view::Pat::Expr(expr) => { + if let ast_view::Expr::Ident(ident) = expr { + self.check_ident(ident, IdentToCheck::variable(ident.inner)); + } + } + ast_view::Pat::Invalid(_) => {} + } + } +} + +impl Handler for CamelcaseHandler { + fn fn_decl(&mut self, fn_decl: &ast_view::FnDecl, ctx: &mut Context) { + if fn_decl.declare() { + ctx.stop_traverse(); + return; + } + + self + .check_ident(&fn_decl.ident, IdentToCheck::function(fn_decl.ident.inner)); + } + + fn class_decl( + &mut self, + class_decl: &ast_view::ClassDecl, + ctx: &mut Context, + ) { + if class_decl.declare() { + ctx.stop_traverse(); + return; + } + + self.check_ident( + &class_decl.ident, + IdentToCheck::class(class_decl.ident.inner), + ); + } + + fn var_decl(&mut self, var_decl: &ast_view::VarDecl, ctx: &mut Context) { + if var_decl.declare() { + ctx.stop_traverse(); + return; + } + + for decl in var_decl.decls { + self.check_pat(&decl.name); + + if let Some(expr) = &decl.init { + match expr { + ast_view::Expr::Object(ast_view::ObjectLit { props, .. }) => { + for prop in *props { + if let ast_view::PropOrSpread::Prop(prop) = prop { + match prop { + ast_view::Prop::Shorthand(ident) => self.check_ident( + ident, + IdentToCheck::object_key(ident.inner, true), + ), + ast_view::Prop::KeyValue(ast_view::KeyValueProp { + ref key, + .. + }) + | ast_view::Prop::Getter(ast_view::GetterProp { + ref key, + .. + }) + | ast_view::Prop::Setter(ast_view::SetterProp { + ref key, + .. + }) + | ast_view::Prop::Method(ast_view::MethodProp { + ref key, + .. + }) => { + if let ast_view::PropName::Ident(ident) = key { + self.check_ident( + ident, + IdentToCheck::object_key(ident.inner, false), + ); + } + } + ast_view::Prop::Assign(_) => {} + } + } + } + } + ast_view::Expr::Fn(ast_view::FnExpr { + ident: Some(ident), .. + }) => { + self.check_ident(ident, IdentToCheck::function(ident.inner)); + } + ast_view::Expr::Class(ast_view::ClassExpr { + ident: Some(ident), + .. + }) => { + self.check_ident(ident, IdentToCheck::class(ident.inner)); + } + _ => {} + } + } + } + } + + fn param(&mut self, param: &ast_view::Param, _ctx: &mut Context) { + self.check_pat(¶m.pat); + } + + fn import_named_specifier( + &mut self, + import_named_specifier: &ast_view::ImportNamedSpecifier, + _ctx: &mut Context, + ) { + let ast_view::ImportNamedSpecifier { + local, imported, .. + } = import_named_specifier; + if let Some(imported) = &imported { + self.check_ident( + local, + IdentToCheck::named_import( + local.inner, + Some(match imported { + ast_view::ModuleExportName::Ident(ident) => ident.sym(), + ast_view::ModuleExportName::Str(str) => str.value(), + }), + ), + ); + } + } + + fn import_default_specifier( + &mut self, + import_default_specifier: &ast_view::ImportDefaultSpecifier, + _ctx: &mut Context, + ) { + let ast_view::ImportDefaultSpecifier { local, .. } = + import_default_specifier; + self.check_ident(local, IdentToCheck::variable(local.inner)); + } + + fn import_star_as_specifier( + &mut self, + import_star_as_specifier: &ast_view::ImportStarAsSpecifier, + _ctx: &mut Context, + ) { + let ast_view::ImportStarAsSpecifier { local, .. } = + import_star_as_specifier; + self.check_ident(local, IdentToCheck::variable(local.inner)); + } + + fn export_namespace_specifier( + &mut self, + export_namespace_specifier: &ast_view::ExportNamespaceSpecifier, + _ctx: &mut Context, + ) { + let ast_view::ExportNamespaceSpecifier { name, .. } = + export_namespace_specifier; + if let ast_view::ModuleExportName::Ident(name) = name { + self.check_ident(name, IdentToCheck::variable(name.inner)); + } + } + + fn ts_type_alias_decl( + &mut self, + type_alias: &ast_view::TsTypeAliasDecl, + ctx: &mut Context, + ) { + if type_alias.declare() { + ctx.stop_traverse(); + return; + } + + self.check_ident( + &type_alias.id, + IdentToCheck::type_alias(type_alias.id.inner), + ); + self.check_ts_type(&type_alias.type_ann); + } + + fn ts_interface_decl( + &mut self, + interface_decl: &ast_view::TsInterfaceDecl, + ctx: &mut Context, + ) { + if interface_decl.declare() { + ctx.stop_traverse(); + return; + } + + self.check_ident( + &interface_decl.id, + IdentToCheck::interface(interface_decl.id.inner), + ); + + for ty_el in interface_decl.body.body { + self.check_ts_type_element(ty_el); + } + } + + fn ts_namespace_decl( + &mut self, + namespace_decl: &ast_view::TsNamespaceDecl, + ctx: &mut Context, + ) { + if namespace_decl.declare() { + ctx.stop_traverse(); + return; + } + + self.check_ident( + &namespace_decl.id, + IdentToCheck::namespace(namespace_decl.id.inner), + ); + } + + fn ts_module_decl( + &mut self, + module_decl: &ast_view::TsModuleDecl, + ctx: &mut Context, + ) { + if module_decl.declare() { + ctx.stop_traverse(); + return; + } + + if let ast_view::TsModuleName::Ident(id) = &module_decl.id { + self.check_ident(id, IdentToCheck::module(id.inner)); + } + } + + fn ts_enum_decl( + &mut self, + enum_decl: &ast_view::TsEnumDecl, + ctx: &mut Context, + ) { + if enum_decl.declare() { + ctx.stop_traverse(); + return; + } + + self + .check_ident(&enum_decl.id, IdentToCheck::enum_name(enum_decl.id.inner)); + for variant in enum_decl.members { + if let ast_view::TsEnumMemberId::Ident(id) = &variant.id { + self.check_ident(id, IdentToCheck::enum_variant(id.inner)); + } + } + } +} + +fn pat_in_var_declarator(pat: Node) -> bool { + for ancestor in pat.ancestors() { + match ancestor.kind() { + NodeKind::VarDeclarator => { + return true; + } + NodeKind::ArrayPat + | NodeKind::ObjectPat + | NodeKind::AssignPat + | NodeKind::AssignPatProp + | NodeKind::RestPat + | NodeKind::KeyValuePatProp => { + // keep going + } + _ => { + return false; + } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_underscored() { + let tests = [ + ("foo_bar", true), + ("fooBar", false), + ("FooBar", false), + ("foo_bar_baz", true), + ("_foo_bar_baz", true), + ("__foo_bar_baz__", true), + ("__fooBar_baz__", true), + ("__fooBarBaz__", false), + ("Sha3_224", true), + ("SHA3_224", false), + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, is_underscored(input)); + } + } + + #[test] + fn test_to_camelcase() { + let tests = [ + ("foo_bar", "fooBar"), + ("fooBar", "fooBar"), + ("FooBar", "FooBar"), + ("foo_bar_baz", "fooBarBaz"), + ("_foo_bar_baz", "_fooBarBaz"), + ("__foo_bar_baz__", "__fooBarBaz__"), + ("Sha3_224", "SHA3_224"), + ("SHA3_224", "SHA3_224"), + ("_leading", "_leading"), + ("trailing_", "trailing_"), + ("_bothEnds_", "_bothEnds_"), + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, to_camelcase(input)); + } + } + + #[test] + fn test_to_hint() { + fn s(s: &str) -> String { + s.to_string() + } + + let tests = [ + ( + IdentToCheck::Variable(s("foo_bar")), + "Consider renaming `foo_bar` to `fooBar`", + ), + ( + IdentToCheck::Function(s("foo_bar")), + "Consider renaming `foo_bar` to `fooBar`", + ), + ( + IdentToCheck::Class(s("foo_bar")), + "Consider renaming `foo_bar` to `FooBar`", + ), + ( + IdentToCheck::ObjectPat { + key_name: s("foo_bar"), + value_name: None, + has_default: false, + is_destructuring: true, + }, + "Consider replacing `{ foo_bar }` with `{ foo_bar: fooBar }`", + ), + ( + IdentToCheck::ObjectPat { + key_name: s("foo_bar"), + value_name: Some(s("snake_case")), + has_default: false, + is_destructuring: true, + }, + "Consider renaming `snake_case` to `snakeCase`", + ), + ( + IdentToCheck::ObjectPat { + key_name: s("foo_bar"), + value_name: None, + has_default: true, + is_destructuring: true, + }, + "Consider replacing `{ foo_bar = .. }` with `{ foo_bar: fooBar = .. }`", + ), + ( + IdentToCheck::ObjectPat { + key_name: s("foo_bar"), + value_name: None, + has_default: true, + is_destructuring: false, + }, + // not destructuring, so suggest a rename + "Consider renaming `foo_bar` to `fooBar`", + ), + ( + IdentToCheck::ObjectPat { + key_name: s("foo_bar"), + value_name: Some(s("snake_case")), + has_default: true, + is_destructuring: true, + }, + "Consider renaming `snake_case` to `snakeCase`", + ), + ( + IdentToCheck::NamedImport { + local: s("foo_bar"), + imported: None, + }, + "Consider replacing `{ foo_bar }` with `{ foo_bar as fooBar }`", + ), + ( + IdentToCheck::NamedImport { + local: s("foo_bar"), + imported: Some(s("snake_case")), + }, + "Consider renaming `foo_bar` to `fooBar`", + ), + ]; + + for (error_ident, expected) in tests.iter() { + assert_eq!(*expected, error_ident.to_hint()); + } + } + + // Based on https://github.com/eslint/eslint/blob/v7.8.1/tests/lib/rules/camelcase.js + + #[test] + fn camelcase_valid() { + assert_lint_ok! { + Camelcase, + r#"firstName = "Ichigo""#, + r#"FIRST_NAME = "Ichigo""#, + r#"__myPrivateVariable = "Hoshimiya""#, + r#"myPrivateVariable_ = "Hoshimiya""#, + r#"function doSomething(){}"#, + r#"do_something()"#, + r#"new do_something"#, + r#"new do_something()"#, + r#"foo.do_something()"#, + r#"var foo = bar.baz_boom;"#, + r#"var foo = bar.baz_boom.something;"#, + r#"foo.boom_pow.qux = bar.baz_boom.something;"#, + r#"if (bar.baz_boom) {}"#, + r#"var obj = { key: foo.bar_baz };"#, + r#"var arr = [foo.bar_baz];"#, + r#"[foo.bar_baz]"#, + r#"var arr = [foo.bar_baz.qux];"#, + r#"[foo.bar_baz.nesting]"#, + r#"if (foo.bar_baz === boom.bam_pow) { [foo.baz_boom] }"#, + r#"var o = {key: 1}"#, + r#"var o = {_leading: 1}"#, + r#"var o = {trailing_: 1}"#, + r#"const { ['foo']: _foo } = obj;"#, + r#"const { [_foo_]: foo } = obj;"#, + r#"var { category_id: category } = query;"#, + r#"var { _leading } = query;"#, + r#"var { trailing_ } = query;"#, + r#"var { or_middle } = query;"#, + r#"var { category_id = 1 } = query;"#, + r#"var { category_id: { property_test } } = query;"#, + r#"const { no_camelcased = false } = bar;"#, + r#"import { camelCased } from "external module";"#, + r#"import { _leading } from "external module";"#, + r#"import { trailing_ } from "external module";"#, + r#"import { or_middle } from "external module";"#, + r#"import { no_camelcased as camelCased } from "external-module";"#, + r#"import { no_camelcased as _leading } from "external-module";"#, + r#"import { no_camelcased as trailing_ } from "external-module";"#, + r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#, + r#"import { camelCased } from 'mod'"#, + r#"var _camelCased = aGlobalVariable"#, + r#"var camelCased = _aGlobalVariable"#, + r#"function foo({ no_camelcased: camelCased }) {};"#, + r#"function foo({ no_camelcased: _leading }) {};"#, + r#"function foo({ no_camelcased: trailing_ }) {};"#, + r#"function foo({ camelCased = 'default value' }) {};"#, + r#"function foo({ _leading = 'default value' }) {};"#, + r#"function foo({ trailing_ = 'default value' }) {};"#, + r#"function foo({ camelCased }) {};"#, + r#"function foo({ _leading }) {}"#, + r#"function foo({ trailing_ }) {}"#, + r#"({obj} = baz.fo_o);"#, + r#"([obj] = baz.fo_o);"#, + r#"([obj.foo = obj.fo_o] = bar);"#, + r#"const f = function camelCased() {};"#, + r#"const c = class camelCased {};"#, + r#"class camelCased {};"#, + + // The following test cases are _invalid_ in ESLint, but we've decided to treat them as _valid_. + // See background at https://github.com/denoland/deno_lint/pull/302 + r#"first_name = "Akari""#, + r#"__private_first_name = "Akari""#, + r#"obj.foo_bar = function(){};"#, + r#"bar_baz.foo = function(){};"#, + r#"[foo_bar.baz]"#, + r#"if (foo.bar_baz === boom.bam_pow) { [foo_bar.baz] }"#, + r#"foo.bar_baz = boom.bam_pow"#, + r#"foo.qux.boom_pow = { bar: boom.bam_pow }"#, + r#"obj.a_b = 2;"#, + r#"var { [category_id]: categoryId } = query;"#, + r#"a_global_variable.foo()"#, + r#"a_global_variable[undefined]"#, + r#"var camelCased = snake_cased"#, + r#"({ a: obj.fo_o } = bar);"#, + r#"({ a: obj.fo_o.b_ar } = baz);"#, + r#"({ a: { b: { c: obj.fo_o } } } = bar);"#, + r#"({ a: { b: { c: obj.fo_o.b_ar } } } = baz);"#, + r#"([obj.fo_o] = bar);"#, + r#"([obj.fo_o = 1] = bar);"#, + r#"({ a: [obj.fo_o] } = bar);"#, + r#"({ a: { b: [obj.fo_o] } } = bar);"#, + r#"([obj.fo_o.ba_r] = baz);"#, + r#"obj.o_k.non_camelcase = 0"#, + r#"(obj?.o_k).non_camelcase = 0"#, + r#"({...obj.fo_o} = baz);"#, + r#"({...obj.fo_o.ba_r} = baz);"#, + r#"({c: {...obj.fo_o }} = baz);"#, + r#"not_ignored_foo = 0;"#, + + // https://github.com/denoland/deno_lint/issues/475 + // We are forced to use snake_case keys in object literals in some cases such as an object + // representing database schema. In such cases, one is allowed to use snake_case by wrapping + // keys in quotation marks. + r#"const obj = { "created_at": "2020-10-30T13:16:45+09:00" }"#, + r#"const obj = { "created_at": created_at }"#, + + // https://github.com/denoland/deno_lint/issues/587 + // The rule shouldn't be applied to ambient declarations since the user who writes them is + // unable to fix their names. + r#"declare function foo_bar(a_b: number): void;"#, + r#"declare const foo_bar: number;"#, + r#"declare const foo_bar: number, snake_case: string;"#, + r#"declare let foo_bar: { some_property: string; };"#, + r#"declare var foo_bar: number;"#, + r#"declare class foo_bar { some_method(some_param: boolean): string; };"#, + r#"export declare const foo_bar: number;"#, + r#"declare type foo_bar = { some_var: string; };"#, + r#"declare interface foo_bar { some_var: string; }"#, + r#"declare namespace foo_bar {}"#, + r#"declare enum foo_bar { variant_one, variant_two }"#, + }; + } + + #[test] + fn camelcase_invalid() { + assert_lint_err! { + Camelcase, + r#"function foo_bar(){}"#: [ + { + col: 9, + message: "Identifier 'foo_bar' is not in camel case.", + hint: "Consider renaming `foo_bar` to `fooBar`", + } + ], + r#"var foo = { bar_baz: boom.bam_pow }"#: [ + { + col: 12, + message: "Identifier 'bar_baz' is not in camel case.", + hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, + } + ], + r#"var o = { bar_baz: 1 }"#: [ + { + col: 10, + message: "Identifier 'bar_baz' is not in camel case.", + hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, + } + ], + r#"var o = { bar_baz }"#: [ + { + col: 10, + message: "Identifier 'bar_baz' is not in camel case.", + hint: r#"Consider writing `barBaz: bar_baz` or `"bar_baz": bar_baz`"#, + } + ], + r#"var { category_id: category_alias } = query;"#: [ + { + col: 19, + message: "Identifier 'category_alias' is not in camel case.", + hint: "Consider renaming `category_alias` to `categoryAlias`", + } + ], + r#"var { category_id: category_id } = query;"#: [ + { + col: 19, + message: "Identifier 'category_id' is not in camel case.", + hint: "Consider renaming `category_id` to `categoryId`", + } + ], + r#"import * as no_camelcased from "external-module";"#: [ + { + col: 12, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"import { no_camelcased as no_camel_cased } from "external module";"#: [ + { + col: 26, + message: "Identifier 'no_camel_cased' is not in camel case.", + hint: "Consider renaming `no_camel_cased` to `noCamelCased`", + } + ], + r#"import { camelCased as no_camel_cased } from "external module";"#: [ + { + col: 23, + message: "Identifier 'no_camel_cased' is not in camel case.", + hint: "Consider renaming `no_camel_cased` to `noCamelCased`", + } + ], + r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#: [ + { + col: 7, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"import snake_cased from 'mod'"#: [ + { + col: 7, + message: "Identifier 'snake_cased' is not in camel case.", + hint: "Consider renaming `snake_cased` to `snakeCased`", + } + ], + r#"import * as snake_cased from 'mod'"#: [ + { + col: 12, + message: "Identifier 'snake_cased' is not in camel case.", + hint: "Consider renaming `snake_cased` to `snakeCased`", + } + ], + r#"export * as snake_cased from 'mod'"#: [ + { + col: 12, + message: "Identifier 'snake_cased' is not in camel case.", + hint: "Consider renaming `snake_cased` to `snakeCased`", + } + ], + r#"function foo({ no_camelcased }) {};"#: [ + { + col: 15, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"function foo({ no_camelcased = 'default value' }) {};"#: [ + { + col: 15, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"const no_camelcased = 0; function foo({ camelcased_value = no_camelcased }) {}"#: [ + { + col: 6, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + }, + { + col: 40, + message: "Identifier 'camelcased_value' is not in camel case.", + hint: "Consider renaming `camelcased_value` to `camelcasedValue`", + } + ], + r#"const { bar: no_camelcased } = foo;"#: [ + { + col: 13, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"function foo({ value_1: my_default }) {}"#: [ + { + col: 24, + message: "Identifier 'my_default' is not in camel case.", + hint: "Consider renaming `my_default` to `myDefault`", + } + ], + r#"function foo({ isCamelcased: no_camelcased }) {};"#: [ + { + col: 29, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"function foo({ isCamelcased: { no_camelcased } }) {};"#: [ + { + col: 31, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"var { foo: bar_baz = 1 } = quz;"#: [ + { + col: 11, + message: "Identifier 'bar_baz' is not in camel case.", + hint: "Consider renaming `bar_baz` to `barBaz`", + } + ], + r#"const f = function no_camelcased() {};"#: [ + { + col: 19, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `noCamelcased`", + } + ], + r#"const c = class no_camelcased {};"#: [ + { + col: 16, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `NoCamelcased`", + } + ], + r#"class no_camelcased {}"#: [ + { + col: 6, + message: "Identifier 'no_camelcased' is not in camel case.", + hint: "Consider renaming `no_camelcased` to `NoCamelcased`", + } + ], + r#"type foo_bar = string;"#: [ + { + col: 5, + message: "Identifier 'foo_bar' is not in camel case.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + r#"type Foo = { snake_case: number; };"#: [ + { + col: 13, + message: "Identifier 'snake_case' is not in camel case.", + hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, + } + ], + r#"interface foo_bar { ok: string; };"#: [ + { + col: 10, + message: "Identifier 'foo_bar' is not in camel case.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + r#"interface Foo { snake_case: number; };"#: [ + { + col: 16, + message: "Identifier 'snake_case' is not in camel case.", + hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, + } + ], + r#"namespace foo_bar {}"#: [ + { + col: 10, + message: "Identifier 'foo_bar' is not in camel case.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + r#"namespace FooBar { const snake_case = 42; }"#: [ + { + col: 25, + message: "Identifier 'snake_case' is not in camel case.", + hint: "Consider renaming `snake_case` to `snakeCase`", + } + ], + r#"enum foo_bar { VariantOne }"#: [ + { + col: 5, + message: "Identifier 'foo_bar' is not in camel case.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + r#"enum FooBar { variant_one }"#: [ + { + col: 14, + message: "Identifier 'variant_one' is not in camel case.", + hint: "Consider renaming `variant_one` to `VariantOne`", + } + ], + }; + } +} From 052391198f50e91461916a11561d0eadc770fc46 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:07:49 +0200 Subject: [PATCH 2/7] feat(rust_style): basic rust-style naming rule --- docs/rules/rust_style.md | 65 +- src/rules.rs | 2 + src/rules/rust_style.rs | 1279 ++++++++++++++++++++++++++++---------- www/static/docs.json | 5 + 4 files changed, 986 insertions(+), 365 deletions(-) diff --git a/docs/rules/rust_style.md b/docs/rules/rust_style.md index f4cd0ce12..3e458cd96 100644 --- a/docs/rules/rust_style.md +++ b/docs/rules/rust_style.md @@ -1,14 +1,18 @@ -Enforces the use of camelCase in variable names +Enforces the use of Rust-style naming conventions, see +https://rust-lang.github.io/api-guidelines/naming.html Consistency in a code base is key for readability and maintainability. This rule -enforces variable declarations and object property names which you create to be -in camelCase. +is useful for deno projects that call rust functions via FFI. It attempts to +unify naming conventions and enforces declarations and object property names +which you create to be\ +in UpperCamelCase/PascalCase for classes, types, interfaces\ +and in snake_case for functions, methods, variables. Of note: - `_` is allowed at the start or end of a variable - All uppercase variable names (e.g. constants) may have `_` in their name -- If you have to use a snake_case key in an object for some reasons, wrap it in +- If you have to use a camelCase key in an object for some reasons, wrap it in quotation mark - This rule also applies to variables imported or exported via ES modules, but not to object properties of those variables @@ -16,58 +20,69 @@ Of note: ### Invalid: ```typescript -let first_name = "Ichigo"; -const obj1 = { last_name: "Hoshimiya" }; -const obj2 = { first_name }; -const { last_name } = obj1; +let firstName = "Ichigo"; +const obj1 = { lastName: "Hoshimiya" }; +const obj2 = { firstName }; +const { lastName } = obj1; -function do_something() {} -function foo({ snake_case = "default value" }) {} +function doSomething() {} +function foo({ camelCase = "default value" }) {} class snake_case_class {} +class camelCaseClass {} class Also_Not_Valid_Class {} -import { not_camelCased } from "external-module.js"; -export * as not_camelCased from "mod.ts"; +import { camelCased } from "external-module.js"; +export * as camelCased from "mod.ts"; enum snake_case_enum { snake_case_variant, } +enum camelCasedEnum { + camelCasedVariant, +} + type snake_case_type = { some_property: number }; +type camelCasedType = { someProperty: number }; + interface snake_case_interface { some_property: number; } + +interface camelCasedInterface { + someProperty: number; +} ``` ### Valid: ```typescript -let firstName = "Ichigo"; +let first_name = "Ichigo"; const FIRST_NAME = "Ichigo"; -const __myPrivateVariable = "Hoshimiya"; -const myPrivateVariable_ = "Hoshimiya"; -const obj1 = { "last_name": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid -const obj2 = { "first_name": first_name }; -const { last_name: lastName } = obj; +const __my_private_variable = "Hoshimiya"; +const my_private_variable_ = "Hoshimiya"; +const obj1 = { "lastName": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid +const obj2 = { "firstName": firstName }; +const { lastName: last_name } = obj; -function doSomething() {} // function declarations must be camelCase but... -do_something(); // ...snake_case function calls are allowed -function foo({ snake_case: camelCase = "default value" }) {} +function do_something() {} // function declarations must be snake_case but... +doSomething(); // ...camel_case function calls are allowed +function foo({ camelCase: snake_case = "default value" }) {} class PascalCaseClass {} -import { not_camelCased as camelCased } from "external-module.js"; -export * as camelCased from "mod.ts"; +import { camelCased as not_camel_cased } from "external-module.js"; +export * as not_camel_cased from "mod.ts"; enum PascalCaseEnum { PascalCaseVariant, } -type PascalCaseType = { someProperty: number }; +type PascalCaseType = { some_property: number }; interface PascalCaseInterface { - someProperty: number; + some_property: number; } ``` diff --git a/src/rules.rs b/src/rules.rs index 4f659d6a6..5c4417159 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -105,6 +105,7 @@ pub mod prefer_namespace_keyword; pub mod prefer_primordials; pub mod require_await; pub mod require_yield; +pub mod rust_style; pub mod single_var_declarator; pub mod triple_slash_reference; pub mod use_isnan; @@ -339,6 +340,7 @@ fn get_all_rules_raw() -> Vec> { Box::new(prefer_primordials::PreferPrimordials), Box::new(require_await::RequireAwait), Box::new(require_yield::RequireYield), + Box::new(rust_style::RustStyle), Box::new(single_var_declarator::SingleVarDeclarator), Box::new(triple_slash_reference::TripleSlashReference), Box::new(use_isnan::UseIsNaN), diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs index e45184491..3e06e3c35 100644 --- a/src/rules/rust_style.rs +++ b/src/rules/rust_style.rs @@ -1,21 +1,24 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// Copyright 2018-2024 the Deno authors + c-antin. All rights reserved. MIT license. +// based on camelcase.rs use super::{Context, LintRule}; use crate::handler::{Handler, Traverse}; -use crate::swc_util::StringRepr; +use crate::swc_util::{find_lhs_ids, StringRepr}; -use deno_ast::view::{Node, NodeKind, NodeTrait}; -use deno_ast::{view as ast_view, SourceRange, SourceRanged}; +use deno_ast::view::{AssignExpr, Node, NodeKind, NodeTrait}; +use deno_ast::{ + view as ast_view, SourceRange, SourceRanged, SourceRangedForSpanned, +}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug)] -pub struct Camelcase; +pub struct RustStyle; -const CODE: &str = "camelcase"; +const CODE: &str = "rust_style"; -impl LintRule for Camelcase { +impl LintRule for RustStyle { fn tags(&self) -> &'static [&'static str] { &[] } @@ -29,14 +32,14 @@ impl LintRule for Camelcase { context: &mut Context, program: ast_view::Program, ) { - let mut handler = CamelcaseHandler::default(); + let mut handler = RustStyleHandler::default(); handler.traverse(program, context); handler.report_errors(context); } #[cfg(feature = "docs")] fn docs(&self) -> &'static str { - include_str!("../../docs/rules/camelcase.md") + include_str!("../../docs/rules/rust_style.md") } } @@ -47,6 +50,30 @@ fn is_underscored(ident_name: &str) -> bool { && trimmed_ident != trimmed_ident.to_ascii_uppercase() } +/// Check if it is snake cased +fn is_snake_cased(ident_name: &str) -> bool { + static UPPERCASE_CHAR_RE: Lazy = + Lazy::new(|| Regex::new(r"[A-Z]").unwrap()); + !UPPERCASE_CHAR_RE.is_match(ident_name) +} + +/// Check if it is screaming snake cased +fn is_screaming_snake_cased(ident_name: &str) -> bool { + static LOWERCASE_CHAR_RE: Lazy = + Lazy::new(|| Regex::new(r"[a-z]").unwrap()); + !LOWERCASE_CHAR_RE.is_match(ident_name) +} + +/// Check if it is upper camel cased +fn is_upper_camel_cased(ident_name: &str) -> bool { + if is_underscored(ident_name) { + return false; + } + static UPPERCASE_FIRST_CHAR_RE: Lazy = + Lazy::new(|| Regex::new(r"^[A-Z]").unwrap()); + UPPERCASE_FIRST_CHAR_RE.is_match(ident_name) +} + /// Convert the name of identifier into camel case. If the name is originally in camel case, return /// the name as it is. For more detail, see the test cases below. fn to_camelcase(ident_name: &str) -> String { @@ -68,6 +95,31 @@ fn to_camelcase(ident_name: &str) -> String { ident_name.to_ascii_uppercase() } +/// Convert the name of identifier into rust style. If the name is originally in rust style, return +/// the name as it is. If name starts with uppercase letter, return as is. For more detail, see the test cases below. +fn to_rust_style(ident_name: &str) -> String { + let trimmed_ident = ident_name.trim_matches('_'); + if let Some(first_char) = trimmed_ident.chars().next() { + if first_char.is_uppercase() { + return ident_name.to_string(); + } + } + + static LOWERCASE_UPPERCASE_CHAR_RE: Lazy = + Lazy::new(|| Regex::new(r"([^A-Z])([A-Z])").unwrap()); + + let result = LOWERCASE_UPPERCASE_CHAR_RE + .replace_all(ident_name, |caps: &Captures| { + format!("{}_{}", &caps[1], caps[2].to_ascii_lowercase()) + }); + + if result != ident_name { + return result.into_owned(); + } + + ident_name.to_string() +} + enum IdentToCheck { /// Normal variable name e.g. `foo` in `const foo = 42;` Variable(String), @@ -234,7 +286,7 @@ impl IdentToCheck { fn to_message(&self) -> String { format!( - "Identifier '{}' is not in camel case.", + "Identifier '{}' is not in rust style.", self.get_ident_name() ) } @@ -242,7 +294,7 @@ impl IdentToCheck { fn to_hint(&self) -> String { match self { IdentToCheck::Variable(name) | IdentToCheck::Function(name) => { - format!("Consider renaming `{}` to `{}`", name, to_camelcase(name)) + format!("Consider renaming `{}` to `{}`", name, to_rust_style(name)) } IdentToCheck::ObjectKey { ref key_name, @@ -250,14 +302,14 @@ impl IdentToCheck { } => { if *is_shorthand { format!( - r#"Consider writing `{camel_cased}: {original}` or `"{original}": {original}`"#, - camel_cased = to_camelcase(key_name), + r#"Consider writing `{rust_styled}: {original}` or `"{original}": {original}`"#, + rust_styled = to_rust_style(key_name), original = key_name ) } else { format!( - r#"Consider renaming `{original}` to `{camel_cased}`, or wrapping it in quotation mark like `"{original}"`"#, - camel_cased = to_camelcase(key_name), + r#"Consider renaming `{original}` to `{rust_styled}`, or wrapping it in quotation mark like `"{original}"`"#, + rust_styled = to_rust_style(key_name), original = key_name ) } @@ -304,7 +356,7 @@ impl IdentToCheck { return format!( "Consider renaming `{}` to `{}`", name, - to_camelcase(name), + to_rust_style(name), ); } @@ -312,24 +364,28 @@ impl IdentToCheck { return format!( "Consider replacing `{{ {key} = .. }}` with `{{ {key}: {value} = .. }}`", key = key_name, - value = to_camelcase(key_name), + value = to_rust_style(key_name), ); } format!( "Consider replacing `{{ {key} }}` with `{{ {key}: {value} }}`", key = key_name, - value = to_camelcase(key_name), + value = to_rust_style(key_name), ) } IdentToCheck::NamedImport { local, imported } => { if imported.is_some() { - format!("Consider renaming `{}` to `{}`", local, to_camelcase(local)) + format!( + "Consider renaming `{}` to `{}`", + local, + to_rust_style(local) + ) } else { format!( - "Consider replacing `{{ {local} }}` with `{{ {local} as {camel_cased_local} }}`", + "Consider replacing `{{ {local} }}` with `{{ {local} as {rust_styled_local} }}`", local = local, - camel_cased_local = to_camelcase(local), + rust_styled_local = to_rust_style(local), ) } } @@ -338,14 +394,14 @@ impl IdentToCheck { } #[derive(Default)] -struct CamelcaseHandler { +struct RustStyleHandler { /// Accumulated errors to report errors: BTreeMap, /// Already visited identifiers visited: BTreeSet, } -impl CamelcaseHandler { +impl RustStyleHandler { /// Report accumulated errors, consuming `self`. fn report_errors(self, ctx: &mut Context) { for (range, error_ident) in self.errors { @@ -358,10 +414,55 @@ impl CamelcaseHandler { } } - /// Check if this ident is underscored only when it's not yet visited. - fn check_ident(&mut self, range: &S, ident: IdentToCheck) { + // /// Check if this ident is not underscored only when it's not yet visited. + // fn check_ident_not_underscored( + // &mut self, + // range: &S, + // ident: IdentToCheck, + // ) { + // let range = range.range(); + // if self.visited.insert(range) && is_underscored(ident.get_ident_name()) { + // self.errors.insert(range, ident); + // } + // } + + /// Check if this ident is snake cased only when it's not yet visited. + fn check_ident_snake_cased( + &mut self, + range: &S, + ident: IdentToCheck, + ) { + let range = range.range(); + if self.visited.insert(range) && !is_snake_cased(ident.get_ident_name()) { + self.errors.insert(range, ident); + } + } + + /// Check if this ident is snake cased or screaming snake cased only when it's not yet visited. + fn check_ident_snake_cased_or_screaming_snake_cased( + &mut self, + range: &S, + ident: IdentToCheck, + ) { + let range = range.range(); + if self.visited.insert(range) + && !is_snake_cased(ident.get_ident_name()) + && !is_screaming_snake_cased(ident.get_ident_name()) + { + self.errors.insert(range, ident); + } + } + + /// Check if this ident is upper camel cased only when it's not yet visited. + fn check_ident_upper_camel_cased( + &mut self, + range: &S, + ident: IdentToCheck, + ) { let range = range.range(); - if self.visited.insert(range) && is_underscored(ident.get_ident_name()) { + if self.visited.insert(range) + && !is_upper_camel_cased(ident.get_ident_name()) + { self.errors.insert(range, ident); } } @@ -379,7 +480,10 @@ impl CamelcaseHandler { match ty_el { TsPropertySignature(prop_sig) => { if let ast_view::Expr::Ident(ident) = prop_sig.key { - self.check_ident(ident, IdentToCheck::object_key(ident.inner, false)); + self.check_ident_snake_cased( + ident, + IdentToCheck::object_key(ident.inner, false), + ); } if let Some(type_ann) = &prop_sig.type_ann { self.check_ts_type(&type_ann.type_ann); @@ -387,7 +491,10 @@ impl CamelcaseHandler { } TsMethodSignature(method_sig) => { if let ast_view::Expr::Ident(ident) = method_sig.key { - self.check_ident(ident, IdentToCheck::function(ident.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::function(ident.inner), + ); } if let Some(type_ann) = &method_sig.type_ann { self.check_ts_type(&type_ann.type_ann); @@ -395,7 +502,10 @@ impl CamelcaseHandler { } TsGetterSignature(getter_sig) => { if let ast_view::Expr::Ident(ident) = getter_sig.key { - self.check_ident(ident, IdentToCheck::function(ident.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::function(ident.inner), + ); } if let Some(type_ann) = &getter_sig.type_ann { self.check_ts_type(&type_ann.type_ann); @@ -403,7 +513,10 @@ impl CamelcaseHandler { } TsSetterSignature(setter_sig) => { if let ast_view::Expr::Ident(ident) = setter_sig.key { - self.check_ident(ident, IdentToCheck::function(ident.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::function(ident.inner), + ); } } TsIndexSignature(_) @@ -415,7 +528,10 @@ impl CamelcaseHandler { fn check_pat(&mut self, pat: &ast_view::Pat) { match pat { ast_view::Pat::Ident(ident) => { - self.check_ident(ident, IdentToCheck::variable(ident.id.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::variable(ident.id.inner), + ); } ast_view::Pat::Array(ast_view::ArrayPat { elems, .. }) => { for pat in elems.iter().flatten() { @@ -434,7 +550,7 @@ impl CamelcaseHandler { .. }) => match value { ast_view::Pat::Ident(value_ident) => { - self.check_ident( + self.check_ident_snake_cased( value_ident, IdentToCheck::object_pat( &key.string_repr().unwrap_or_else(|| "[KEY]".to_string()), @@ -448,7 +564,7 @@ impl CamelcaseHandler { left: ast_view::Pat::Ident(value_ident), .. }) => { - self.check_ident( + self.check_ident_snake_cased( value_ident, IdentToCheck::object_pat( &key.string_repr().unwrap_or_else(|| "[KEY]".to_string()), @@ -470,7 +586,7 @@ impl CamelcaseHandler { let has_default = value.is_some(); let in_var_declarator = pat_in_var_declarator(pat.into()); if !in_var_declarator { - self.check_ident( + self.check_ident_snake_cased( key, IdentToCheck::object_pat::<&str, &str>( &key.inner.as_ref(), @@ -495,7 +611,10 @@ impl CamelcaseHandler { } ast_view::Pat::Expr(expr) => { if let ast_view::Expr::Ident(ident) = expr { - self.check_ident(ident, IdentToCheck::variable(ident.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::variable(ident.inner), + ); } } ast_view::Pat::Invalid(_) => {} @@ -503,15 +622,28 @@ impl CamelcaseHandler { } } -impl Handler for CamelcaseHandler { +impl Handler for RustStyleHandler { + fn assign_expr(&mut self, e: &AssignExpr, _ctx: &mut Context) { + let idents: Vec = find_lhs_ids(&e.left); + + for ident in idents { + self.check_ident_snake_cased_or_screaming_snake_cased( + &ident.range(), + IdentToCheck::variable(ident.to_id().0), + ); + } + } + fn fn_decl(&mut self, fn_decl: &ast_view::FnDecl, ctx: &mut Context) { if fn_decl.declare() { ctx.stop_traverse(); return; } - self - .check_ident(&fn_decl.ident, IdentToCheck::function(fn_decl.ident.inner)); + self.check_ident_snake_cased( + &fn_decl.ident, + IdentToCheck::function(fn_decl.ident.inner), + ); } fn class_decl( @@ -524,7 +656,7 @@ impl Handler for CamelcaseHandler { return; } - self.check_ident( + self.check_ident_upper_camel_cased( &class_decl.ident, IdentToCheck::class(class_decl.ident.inner), ); @@ -545,10 +677,11 @@ impl Handler for CamelcaseHandler { for prop in *props { if let ast_view::PropOrSpread::Prop(prop) = prop { match prop { - ast_view::Prop::Shorthand(ident) => self.check_ident( - ident, - IdentToCheck::object_key(ident.inner, true), - ), + ast_view::Prop::Shorthand(ident) => self + .check_ident_snake_cased( + ident, + IdentToCheck::object_key(ident.inner, true), + ), ast_view::Prop::KeyValue(ast_view::KeyValueProp { ref key, .. @@ -566,7 +699,7 @@ impl Handler for CamelcaseHandler { .. }) => { if let ast_view::PropName::Ident(ident) = key { - self.check_ident( + self.check_ident_snake_cased( ident, IdentToCheck::object_key(ident.inner, false), ); @@ -580,13 +713,19 @@ impl Handler for CamelcaseHandler { ast_view::Expr::Fn(ast_view::FnExpr { ident: Some(ident), .. }) => { - self.check_ident(ident, IdentToCheck::function(ident.inner)); + self.check_ident_snake_cased( + ident, + IdentToCheck::function(ident.inner), + ); } ast_view::Expr::Class(ast_view::ClassExpr { ident: Some(ident), .. }) => { - self.check_ident(ident, IdentToCheck::class(ident.inner)); + self.check_ident_upper_camel_cased( + ident, + IdentToCheck::class(ident.inner), + ); } _ => {} } @@ -607,7 +746,7 @@ impl Handler for CamelcaseHandler { local, imported, .. } = import_named_specifier; if let Some(imported) = &imported { - self.check_ident( + self.check_ident_snake_cased( local, IdentToCheck::named_import( local.inner, @@ -627,7 +766,7 @@ impl Handler for CamelcaseHandler { ) { let ast_view::ImportDefaultSpecifier { local, .. } = import_default_specifier; - self.check_ident(local, IdentToCheck::variable(local.inner)); + self.check_ident_snake_cased(local, IdentToCheck::variable(local.inner)); } fn import_star_as_specifier( @@ -637,7 +776,7 @@ impl Handler for CamelcaseHandler { ) { let ast_view::ImportStarAsSpecifier { local, .. } = import_star_as_specifier; - self.check_ident(local, IdentToCheck::variable(local.inner)); + self.check_ident_snake_cased(local, IdentToCheck::variable(local.inner)); } fn export_namespace_specifier( @@ -648,7 +787,7 @@ impl Handler for CamelcaseHandler { let ast_view::ExportNamespaceSpecifier { name, .. } = export_namespace_specifier; if let ast_view::ModuleExportName::Ident(name) = name { - self.check_ident(name, IdentToCheck::variable(name.inner)); + self.check_ident_snake_cased(name, IdentToCheck::variable(name.inner)); } } @@ -662,7 +801,7 @@ impl Handler for CamelcaseHandler { return; } - self.check_ident( + self.check_ident_upper_camel_cased( &type_alias.id, IdentToCheck::type_alias(type_alias.id.inner), ); @@ -679,7 +818,7 @@ impl Handler for CamelcaseHandler { return; } - self.check_ident( + self.check_ident_upper_camel_cased( &interface_decl.id, IdentToCheck::interface(interface_decl.id.inner), ); @@ -699,7 +838,7 @@ impl Handler for CamelcaseHandler { return; } - self.check_ident( + self.check_ident_upper_camel_cased( &namespace_decl.id, IdentToCheck::namespace(namespace_decl.id.inner), ); @@ -716,7 +855,7 @@ impl Handler for CamelcaseHandler { } if let ast_view::TsModuleName::Ident(id) = &module_decl.id { - self.check_ident(id, IdentToCheck::module(id.inner)); + self.check_ident_upper_camel_cased(id, IdentToCheck::module(id.inner)); } } @@ -730,11 +869,16 @@ impl Handler for CamelcaseHandler { return; } - self - .check_ident(&enum_decl.id, IdentToCheck::enum_name(enum_decl.id.inner)); + self.check_ident_upper_camel_cased( + &enum_decl.id, + IdentToCheck::enum_name(enum_decl.id.inner), + ); for variant in enum_decl.members { if let ast_view::TsEnumMemberId::Ident(id) = &variant.id { - self.check_ident(id, IdentToCheck::enum_variant(id.inner)); + self.check_ident_upper_camel_cased( + id, + IdentToCheck::enum_variant(id.inner), + ); } } } @@ -769,16 +913,16 @@ mod tests { #[test] fn test_is_underscored() { let tests = [ - ("foo_bar", true), - ("fooBar", false), - ("FooBar", false), - ("foo_bar_baz", true), - ("_foo_bar_baz", true), - ("__foo_bar_baz__", true), - ("__fooBar_baz__", true), - ("__fooBarBaz__", false), - ("Sha3_224", true), - ("SHA3_224", false), + ("foo_bar", true), //snake + ("fooBar", false), //camel + ("FooBar", false), //upper camel + ("foo_bar_baz", true), //snake + ("_foo_bar_baz", true), //snake + ("__foo_bar_baz__", true), //snake + ("__fooBar_baz__", true), //snake + ("__fooBarBaz__", false), //camel + ("Sha3_224", true), //not snake + ("SHA3_224", false), //screaming snake ]; for &(input, expected) in tests.iter() { @@ -786,6 +930,66 @@ mod tests { } } + #[test] + fn test_is_snake_cased() { + let tests = [ + ("foo_bar", true), //snake + ("fooBar", false), //camel + ("FooBar", false), //upper camel + ("foo_bar_baz", true), //snake + ("_foo_bar_baz", true), //snake + ("__foo_bar_baz__", true), //snake + ("__fooBar_baz__", false), //not snake + ("__fooBarBaz__", false), //camel + ("Sha3_224", false), //not snake + ("SHA3_224", false), //screaming snake + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, is_snake_cased(input)); + } + } + + #[test] + fn test_is_screaming_snake_cased() { + let tests = [ + ("foo_bar", false), //snake + ("fooBar", false), //camel + ("FooBar", false), //upper camel + ("foo_bar_baz", false), //snake + ("_foo_bar_baz", false), //snake + ("__foo_bar_baz__", false), //snake + ("__fooBar_baz__", false), //not snake + ("__fooBarBaz__", false), //camel + ("Sha3_224", false), //not snake + ("SHA3_224", true), //screaming snake + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, is_screaming_snake_cased(input)); + } + } + + #[test] + fn test_is_upper_camel_cased() { + let tests = [ + ("foo_bar", false), //snake + ("fooBar", false), //camel + ("FooBar", true), //upper camel + ("foo_bar_baz", false), //snake + ("_foo_bar_baz", false), //snake + ("__foo_bar_baz__", false), //snake + ("__fooBar_baz__", false), //not snake + ("__fooBarBaz__", false), //camel + ("Sha3_224", false), //not snake + ("SHA3_224", true), //screaming snake; todo: should this be true? + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, is_upper_camel_cased(input)); + } + } + #[test] fn test_to_camelcase() { let tests = [ @@ -807,6 +1011,28 @@ mod tests { } } + #[test] + fn test_to_rust_style() { + let tests = [ + ("fooBar", "foo_bar"), + ("foo_bar", "foo_bar"), + ("FooBar", "FooBar"), + ("_FooBar", "_FooBar"), + ("fooBarBaz", "foo_bar_baz"), + ("_fooBarBaz", "_foo_bar_baz"), + ("__fooBarBaz__", "__foo_bar_baz__"), + ("Sha3_224", "Sha3_224"), + ("SHA3_224", "SHA3_224"), + ("_leading", "_leading"), + ("trailing_", "trailing_"), + ("_bothends_", "_bothends_"), + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, to_rust_style(input)); + } + } + #[test] fn test_to_hint() { fn s(s: &str) -> String { @@ -815,12 +1041,12 @@ mod tests { let tests = [ ( - IdentToCheck::Variable(s("foo_bar")), - "Consider renaming `foo_bar` to `fooBar`", + IdentToCheck::Variable(s("fooBar")), + "Consider renaming `fooBar` to `foo_bar`", ), ( - IdentToCheck::Function(s("foo_bar")), - "Consider renaming `foo_bar` to `fooBar`", + IdentToCheck::Function(s("fooBar")), + "Consider renaming `fooBar` to `foo_bar`", ), ( IdentToCheck::Class(s("foo_bar")), @@ -828,63 +1054,63 @@ mod tests { ), ( IdentToCheck::ObjectPat { - key_name: s("foo_bar"), + key_name: s("fooBar"), value_name: None, has_default: false, is_destructuring: true, }, - "Consider replacing `{ foo_bar }` with `{ foo_bar: fooBar }`", + "Consider replacing `{ fooBar }` with `{ fooBar: foo_bar }`", ), ( IdentToCheck::ObjectPat { - key_name: s("foo_bar"), - value_name: Some(s("snake_case")), + key_name: s("fooBar"), + value_name: Some(s("camelCase")), has_default: false, is_destructuring: true, }, - "Consider renaming `snake_case` to `snakeCase`", + "Consider renaming `camelCase` to `camel_case`", ), ( IdentToCheck::ObjectPat { - key_name: s("foo_bar"), + key_name: s("fooBar"), value_name: None, has_default: true, is_destructuring: true, }, - "Consider replacing `{ foo_bar = .. }` with `{ foo_bar: fooBar = .. }`", + "Consider replacing `{ fooBar = .. }` with `{ fooBar: foo_bar = .. }`", ), ( IdentToCheck::ObjectPat { - key_name: s("foo_bar"), + key_name: s("fooBar"), value_name: None, has_default: true, is_destructuring: false, }, // not destructuring, so suggest a rename - "Consider renaming `foo_bar` to `fooBar`", + "Consider renaming `fooBar` to `foo_bar`", ), ( IdentToCheck::ObjectPat { key_name: s("foo_bar"), - value_name: Some(s("snake_case")), + value_name: Some(s("camelCase")), has_default: true, is_destructuring: true, }, - "Consider renaming `snake_case` to `snakeCase`", + "Consider renaming `camelCase` to `camel_case`", ), ( IdentToCheck::NamedImport { - local: s("foo_bar"), + local: s("fooBar"), imported: None, }, - "Consider replacing `{ foo_bar }` with `{ foo_bar as fooBar }`", + "Consider replacing `{ fooBar }` with `{ fooBar as foo_bar }`", ), ( IdentToCheck::NamedImport { - local: s("foo_bar"), - imported: Some(s("snake_case")), + local: s("fooBar"), + imported: Some(s("camelCase")), }, - "Consider renaming `foo_bar` to `fooBar`", + "Consider renaming `fooBar` to `foo_bar`", ), ]; @@ -896,17 +1122,22 @@ mod tests { // Based on https://github.com/eslint/eslint/blob/v7.8.1/tests/lib/rules/camelcase.js #[test] - fn camelcase_valid() { + fn rust_style_valid() { assert_lint_ok! { - Camelcase, - r#"firstName = "Ichigo""#, + RustStyle, + // r#"firstName = "Ichigo""#,// see rust_style_invalid below + r#"first_name = "Ichigo""#,// new r#"FIRST_NAME = "Ichigo""#, - r#"__myPrivateVariable = "Hoshimiya""#, - r#"myPrivateVariable_ = "Hoshimiya""#, - r#"function doSomething(){}"#, - r#"do_something()"#, - r#"new do_something"#, - r#"new do_something()"#, + // r#"__myPrivateVariable = "Hoshimiya""#,// see rust_style_invalid below + r#"__my_private_variable = "Hoshimiya""#,// new + // r#"myPrivateVariable_ = "Hoshimiya""#,// see rust_style_invalid below + r#"my_private_variable_ = "Hoshimiya""#,// new + // r#"function doSomething(){}"#,// see rust_style_invalid below + r#"do_something()"#,// new + r#"new do_something"#,// still valid, if external class + r#"new DoSomething"#,// new + r#"new do_something()"#,// still valid, if external class + r#"new DoSomething()"#,// new r#"foo.do_something()"#, r#"var foo = bar.baz_boom;"#, r#"var foo = bar.baz_boom.something;"#, @@ -934,28 +1165,39 @@ mod tests { r#"import { _leading } from "external module";"#, r#"import { trailing_ } from "external module";"#, r#"import { or_middle } from "external module";"#, - r#"import { no_camelcased as camelCased } from "external-module";"#, + // r#"import { no_camelcased as camelCased } from "external-module";"#,// see rust_style_invalid below + r#"import { camelCased as no_camelcased } from "external-module";"#,// new r#"import { no_camelcased as _leading } from "external-module";"#, r#"import { no_camelcased as trailing_ } from "external-module";"#, - r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#, - r#"import { camelCased } from 'mod'"#, - r#"var _camelCased = aGlobalVariable"#, - r#"var camelCased = _aGlobalVariable"#, - r#"function foo({ no_camelcased: camelCased }) {};"#, + // r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#,// see rust_style_invalid below + r#"import { camelCased as no_camelcased, anotherCamelCased } from "external-module";"#,// new + r#"import { camelCased } from 'mod'"#,// still valid, if external module + r#"import { no_camelcased } from 'mod'"#,// new + // r#"var _camelCased = aGlobalVariable"#,// see rust_style_invalid below + r#"var _no_camelcased = aGlobalVariable"#,// new + // r#"var camelCased = _aGlobalVariable"#,// see rust_style_invalid below + r#"var no_camelcased = _aGlobalVariable"#,// new + // r#"function foo({ no_camelcased: camelCased }) {};"#,// see rust_style_invalid below + r#"function foo({ camelCased: no_camelcased }) {};"#,// new r#"function foo({ no_camelcased: _leading }) {};"#, r#"function foo({ no_camelcased: trailing_ }) {};"#, - r#"function foo({ camelCased = 'default value' }) {};"#, + // r#"function foo({ camelCased = 'default value' }) {};"#,// see rust_style_invalid below + r#"function foo({ no_camelcased = 'default value' }) {};"#,// new r#"function foo({ _leading = 'default value' }) {};"#, r#"function foo({ trailing_ = 'default value' }) {};"#, - r#"function foo({ camelCased }) {};"#, + // r#"function foo({ camelCased }) {};"#,// see rust_style_invalid below + r#"function foo({ no_camelcased }) {};"#,// new r#"function foo({ _leading }) {}"#, r#"function foo({ trailing_ }) {}"#, r#"({obj} = baz.fo_o);"#, r#"([obj] = baz.fo_o);"#, r#"([obj.foo = obj.fo_o] = bar);"#, - r#"const f = function camelCased() {};"#, - r#"const c = class camelCased {};"#, - r#"class camelCased {};"#, + // r#"const f = function camelCased() {};"#,// see rust_style_invalid below + r#"const f = function no_camelcased() {};"#,// new + // r#"const c = class camelCased {};"#,// see rust_style_invalid below + r#"const c = class UpperCamelCased {};"#,// new + // r#"class camelCased {};"#,// see rust_style_invalid below + r#"class UpperCamelCased {};"#,// new // The following test cases are _invalid_ in ESLint, but we've decided to treat them as _valid_. // See background at https://github.com/denoland/deno_lint/pull/302 @@ -968,10 +1210,12 @@ mod tests { r#"foo.bar_baz = boom.bam_pow"#, r#"foo.qux.boom_pow = { bar: boom.bam_pow }"#, r#"obj.a_b = 2;"#, - r#"var { [category_id]: categoryId } = query;"#, + // r#"var { [category_id]: categoryId } = query;"#,// see rust_style_invalid below + r#"var { [categoryId]: category_id } = query;"#,// new r#"a_global_variable.foo()"#, r#"a_global_variable[undefined]"#, - r#"var camelCased = snake_cased"#, + // r#"var camelCased = snake_cased"#,// see rust_style_invalid below + r#"var snake_cased = camelCased"#,// new r#"({ a: obj.fo_o } = bar);"#, r#"({ a: obj.fo_o.b_ar } = baz);"#, r#"({ a: { b: { c: obj.fo_o } } } = bar);"#, @@ -993,258 +1237,613 @@ mod tests { // representing database schema. In such cases, one is allowed to use snake_case by wrapping // keys in quotation marks. r#"const obj = { "created_at": "2020-10-30T13:16:45+09:00" }"#, + r#"const obj = { "createdAt": "2020-10-30T13:16:45+09:00" }"#,// new r#"const obj = { "created_at": created_at }"#, + r#"const obj = { "createdAt": created_at }"#,// new // https://github.com/denoland/deno_lint/issues/587 // The rule shouldn't be applied to ambient declarations since the user who writes them is // unable to fix their names. r#"declare function foo_bar(a_b: number): void;"#, + r#"declare function fooBar(aB: number): void;"#,// new r#"declare const foo_bar: number;"#, + r#"declare const fooBar: number;"#,// new r#"declare const foo_bar: number, snake_case: string;"#, + r#"declare const fooBar: number, camelCase: string;"#,// new r#"declare let foo_bar: { some_property: string; };"#, + r#"declare let fooBar: { someProperty: string; };"#,// new r#"declare var foo_bar: number;"#, + r#"declare var fooBar: number;"#,// new r#"declare class foo_bar { some_method(some_param: boolean): string; };"#, + r#"declare class fooBar { someMethod(someParam: boolean): string; };"#,// new r#"export declare const foo_bar: number;"#, + r#"export declare const fooBar: number;"#,// new r#"declare type foo_bar = { some_var: string; };"#, + r#"declare type fooBar = { someVar: string; };"#,// new r#"declare interface foo_bar { some_var: string; }"#, + r#"declare interface fooBar { someVar: string; }"#,// new r#"declare namespace foo_bar {}"#, + r#"declare namespace fooBar {}"#,// new r#"declare enum foo_bar { variant_one, variant_two }"#, + r#"declare enum fooVar { variantOne, variantTwo }"#,// new + + //new valid test cases: + r#"function foo_bar(){}"#, + r#"var foo = { bar_baz: boom.bam_pow }"#, + r#"var o = { bar_baz: 1 }"#, + r#"var o = { bar_baz }"#, + r#"var { category_id: category_alias } = query;"#, + r#"var { category_id: category_id } = query;"#, + r#"import * as no_camelcased from "external-module";"#, + r#"import { no_camelcased as no_camel_cased } from "external module";"#, + r#"import { camelCased as no_camel_cased } from "external module";"#, + r#"import no_camelcased, { anotherCamelCased as another_no_camelcased } from "external-module";"#,// new + r#"import snake_cased from 'mod'"#, + r#"import * as snake_cased from 'mod'"#, + r#"export * as snake_cased from 'mod'"#, + r#"function foo({ no_camelcased }) {};"#, + r#"function foo({ no_camelcased = 'default value' }) {};"#, + r#"const no_camelcased = 0; function foo({ camelcased_value = no_camelcased }) {}"#, + r#"const { bar: no_camelcased } = foo;"#, + r#"function foo({ value_1: my_default }) {}"#, + r#"function foo({ isCamelcased: no_camelcased }) {};"#, + r#"function foo({ isCamelcased: { no_camelcased } }) {};"#, + r#"var { foo: bar_baz = 1 } = quz;"#, + r#"const f = function no_camelcased() {};"#, + r#"type Foo = { snake_case: number; };"#, + r#"interface Foo { snake_case: number; };"#, + r#"namespace FooBar { const snake_case = 42; }"#, }; } #[test] - fn camelcase_invalid() { + fn rust_style_invalid() { assert_lint_err! { - Camelcase, - r#"function foo_bar(){}"#: [ - { - col: 9, - message: "Identifier 'foo_bar' is not in camel case.", - hint: "Consider renaming `foo_bar` to `fooBar`", - } - ], - r#"var foo = { bar_baz: boom.bam_pow }"#: [ - { - col: 12, - message: "Identifier 'bar_baz' is not in camel case.", - hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, - } - ], - r#"var o = { bar_baz: 1 }"#: [ - { - col: 10, - message: "Identifier 'bar_baz' is not in camel case.", - hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, - } - ], - r#"var o = { bar_baz }"#: [ - { - col: 10, - message: "Identifier 'bar_baz' is not in camel case.", - hint: r#"Consider writing `barBaz: bar_baz` or `"bar_baz": bar_baz`"#, - } - ], - r#"var { category_id: category_alias } = query;"#: [ - { - col: 19, - message: "Identifier 'category_alias' is not in camel case.", - hint: "Consider renaming `category_alias` to `categoryAlias`", - } - ], - r#"var { category_id: category_id } = query;"#: [ - { - col: 19, - message: "Identifier 'category_id' is not in camel case.", - hint: "Consider renaming `category_id` to `categoryId`", - } - ], - r#"import * as no_camelcased from "external-module";"#: [ - { - col: 12, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"import { no_camelcased as no_camel_cased } from "external module";"#: [ - { - col: 26, - message: "Identifier 'no_camel_cased' is not in camel case.", - hint: "Consider renaming `no_camel_cased` to `noCamelCased`", - } - ], - r#"import { camelCased as no_camel_cased } from "external module";"#: [ - { - col: 23, - message: "Identifier 'no_camel_cased' is not in camel case.", - hint: "Consider renaming `no_camel_cased` to `noCamelCased`", - } - ], - r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#: [ - { - col: 7, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"import snake_cased from 'mod'"#: [ - { - col: 7, - message: "Identifier 'snake_cased' is not in camel case.", - hint: "Consider renaming `snake_cased` to `snakeCased`", - } - ], - r#"import * as snake_cased from 'mod'"#: [ - { - col: 12, - message: "Identifier 'snake_cased' is not in camel case.", - hint: "Consider renaming `snake_cased` to `snakeCased`", - } - ], - r#"export * as snake_cased from 'mod'"#: [ - { - col: 12, - message: "Identifier 'snake_cased' is not in camel case.", - hint: "Consider renaming `snake_cased` to `snakeCased`", - } - ], - r#"function foo({ no_camelcased }) {};"#: [ - { - col: 15, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"function foo({ no_camelcased = 'default value' }) {};"#: [ - { - col: 15, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"const no_camelcased = 0; function foo({ camelcased_value = no_camelcased }) {}"#: [ - { - col: 6, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - }, - { - col: 40, - message: "Identifier 'camelcased_value' is not in camel case.", - hint: "Consider renaming `camelcased_value` to `camelcasedValue`", - } - ], - r#"const { bar: no_camelcased } = foo;"#: [ - { - col: 13, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"function foo({ value_1: my_default }) {}"#: [ - { - col: 24, - message: "Identifier 'my_default' is not in camel case.", - hint: "Consider renaming `my_default` to `myDefault`", - } - ], - r#"function foo({ isCamelcased: no_camelcased }) {};"#: [ - { - col: 29, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"function foo({ isCamelcased: { no_camelcased } }) {};"#: [ - { - col: 31, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"var { foo: bar_baz = 1 } = quz;"#: [ - { - col: 11, - message: "Identifier 'bar_baz' is not in camel case.", - hint: "Consider renaming `bar_baz` to `barBaz`", - } - ], - r#"const f = function no_camelcased() {};"#: [ - { - col: 19, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `noCamelcased`", - } - ], - r#"const c = class no_camelcased {};"#: [ - { - col: 16, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `NoCamelcased`", - } - ], - r#"class no_camelcased {}"#: [ - { - col: 6, - message: "Identifier 'no_camelcased' is not in camel case.", - hint: "Consider renaming `no_camelcased` to `NoCamelcased`", - } - ], - r#"type foo_bar = string;"#: [ - { - col: 5, - message: "Identifier 'foo_bar' is not in camel case.", - hint: "Consider renaming `foo_bar` to `FooBar`", - } - ], - r#"type Foo = { snake_case: number; };"#: [ - { - col: 13, - message: "Identifier 'snake_case' is not in camel case.", - hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, - } - ], - r#"interface foo_bar { ok: string; };"#: [ - { - col: 10, - message: "Identifier 'foo_bar' is not in camel case.", - hint: "Consider renaming `foo_bar` to `FooBar`", - } - ], - r#"interface Foo { snake_case: number; };"#: [ - { - col: 16, - message: "Identifier 'snake_case' is not in camel case.", - hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, - } - ], - r#"namespace foo_bar {}"#: [ - { - col: 10, - message: "Identifier 'foo_bar' is not in camel case.", - hint: "Consider renaming `foo_bar` to `FooBar`", - } - ], - r#"namespace FooBar { const snake_case = 42; }"#: [ - { - col: 25, - message: "Identifier 'snake_case' is not in camel case.", - hint: "Consider renaming `snake_case` to `snakeCase`", - } - ], - r#"enum foo_bar { VariantOne }"#: [ - { - col: 5, - message: "Identifier 'foo_bar' is not in camel case.", - hint: "Consider renaming `foo_bar` to `FooBar`", - } - ], - r#"enum FooBar { variant_one }"#: [ - { - col: 14, - message: "Identifier 'variant_one' is not in camel case.", - hint: "Consider renaming `variant_one` to `VariantOne`", - } - ], + RustStyle, + // r#"function foo_bar(){}"#: [ + // { + // col: 9, + // message: "Identifier 'foo_bar' is not in rust style.", + // hint: "Consider renaming `foo_bar` to `fooBar`", + // } + // ], + r#"function fooBar(){}"#: [// new + { + col: 9, + message: "Identifier 'fooBar' is not in rust style.", + hint: "Consider renaming `fooBar` to `foo_bar`", + } + ], + // r#"var foo = { bar_baz: boom.bam_pow }"#: [ + // { + // col: 12, + // message: "Identifier 'bar_baz' is not in rust style.", + // hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, + // } + // ], + r#"var foo = { barBaz: boom.bam_pow }"#: [// new + { + col: 12, + message: "Identifier 'barBaz' is not in rust style.", + hint: r#"Consider renaming `barBaz` to `bar_baz`, or wrapping it in quotation mark like `"barBaz"`"#, + } + ], + // r#"var o = { bar_baz: 1 }"#: [ + // { + // col: 10, + // message: "Identifier 'bar_baz' is not in rust style.", + // hint: r#"Consider renaming `bar_baz` to `barBaz`, or wrapping it in quotation mark like `"bar_baz"`"#, + // } + // ], + r#"var o = { barBaz: 1 }"#: [// new + { + col: 10, + message: "Identifier 'barBaz' is not in rust style.", + hint: r#"Consider renaming `barBaz` to `bar_baz`, or wrapping it in quotation mark like `"barBaz"`"#, + } + ], + // r#"var o = { bar_baz }"#: [ + // { + // col: 10, + // message: "Identifier 'bar_baz' is not in rust style.", + // hint: r#"Consider writing `barBaz: bar_baz` or `"bar_baz": bar_baz`"#, + // } + // ], + r#"var o = { barBaz }"#: [// new + { + col: 10, + message: "Identifier 'barBaz' is not in rust style.", + hint: r#"Consider writing `bar_baz: barBaz` or `"barBaz": barBaz`"#, + } + ], + // r#"var { category_id: category_alias } = query;"#: [ + // { + // col: 19, + // message: "Identifier 'category_alias' is not in rust style.", + // hint: "Consider renaming `category_alias` to `categoryAlias`", + // } + // ], + r#"var { category_id: categoryAlias } = query;"#: [// new + { + col: 19, + message: "Identifier 'categoryAlias' is not in rust style.", + hint: "Consider renaming `categoryAlias` to `category_alias`", + } + ], + // r#"var { category_id: category_id } = query;"#: [ + // { + // col: 19, + // message: "Identifier 'category_id' is not in rust style.", + // hint: "Consider renaming `category_id` to `categoryId`", + // } + // ], + r#"var { category_id: categoryId } = query;"#: [// new + { + col: 19, + message: "Identifier 'categoryId' is not in rust style.", + hint: "Consider renaming `categoryId` to `category_id`", + } + ], + // r#"import * as no_camelcased from "external-module";"#: [ + // { + // col: 12, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"import * as camelCased from "external-module";"#: [// new + { + col: 12, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"import { no_camelcased as no_camel_cased } from "external module";"#: [ + // { + // col: 26, + // message: "Identifier 'no_camel_cased' is not in rust style.", + // hint: "Consider renaming `no_camel_cased` to `noCamelCased`", + // } + // ], + r#"import { no_camelcased as camelCased } from "external module";"#: [// new + { + col: 26, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"import { camelCased as no_camel_cased } from "external module";"#: [ + // { + // col: 23, + // message: "Identifier 'no_camel_cased' is not in rust style.", + // hint: "Consider renaming `no_camel_cased` to `noCamelCased`", + // } + // ], + r#"import { camelCased as camelCased } from "external module";"#: [// new + { + col: 23, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#: [ + // { + // col: 7, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"import camelCased, { anotherCamelCased as no_camelcased } from "external-module";"#: [// new + { + col: 7, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"import no_camelcased, { another_no_camelcased as camelCased } from "external-module";"#: [// new + { + col: 49, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"import snake_cased from 'mod'"#: [ + // { + // col: 7, + // message: "Identifier 'snake_cased' is not in rust style.", + // hint: "Consider renaming `snake_cased` to `snakeCased`", + // } + // ], + r#"import camelCased from 'mod'"#: [// new + { + col: 7, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"import * as snake_cased from 'mod'"#: [ + // { + // col: 12, + // message: "Identifier 'snake_cased' is not in rust style.", + // hint: "Consider renaming `snake_cased` to `snakeCased`", + // } + // ], + r#"import * as camelCased from 'mod'"#: [// new + { + col: 12, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"export * as snake_cased from 'mod'"#: [ + // { + // col: 12, + // message: "Identifier 'snake_cased' is not in rust style.", + // hint: "Consider renaming `snake_cased` to `snakeCased`", + // } + // ], + r#"export * as camelCased from 'mod'"#: [// new + { + col: 12, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"function foo({ no_camelcased }) {};"#: [ + // { + // col: 15, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"function foo({ camelCased }) {};"#: [// new + { + col: 15, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"function foo({ no_camelcased = 'default value' }) {};"#: [ + // { + // col: 15, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"function foo({ camelCased = 'default value' }) {};"#: [// new + { + col: 15, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"const no_camelcased = 0; function foo({ camelcased_value = no_camelcased }) {}"#: [ + // { + // col: 6, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // }, + // { + // col: 40, + // message: "Identifier 'camelcased_value' is not in rust style.", + // hint: "Consider renaming `camelcased_value` to `camelcasedValue`", + // } + // ], + r#"const camelCased = 0; function foo({ camelCased_value = no_camelcased }) {}"#: [// new + { + col: 6, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + }, + { + col: 37, + message: "Identifier 'camelCased_value' is not in rust style.", + hint: "Consider renaming `camelCased_value` to `camel_cased_value`", + } + ], + // r#"const { bar: no_camelcased } = foo;"#: [ + // { + // col: 13, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"const { bar: camelCased } = foo;"#: [// new + { + col: 13, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"function foo({ value_1: my_default }) {}"#: [ + // { + // col: 24, + // message: "Identifier 'my_default' is not in rust style.", + // hint: "Consider renaming `my_default` to `myDefault`", + // } + // ], + r#"function foo({ value_1: myDefault }) {}"#: [// new + { + col: 24, + message: "Identifier 'myDefault' is not in rust style.", + hint: "Consider renaming `myDefault` to `my_default`", + } + ], + // r#"function foo({ isCamelcased: no_camelcased }) {};"#: [ + // { + // col: 29, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"function foo({ is_not_camelcased: camelCased }) {};"#: [// new + { + col: 34, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"function foo({ isCamelcased: { no_camelcased } }) {};"#: [ + // { + // col: 31, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"function foo({ is_not_camelcased: { camelCased } }) {};"#: [// new + { + col: 36, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + // r#"var { foo: bar_baz = 1 } = quz;"#: [ + // { + // col: 11, + // message: "Identifier 'bar_baz' is not in rust style.", + // hint: "Consider renaming `bar_baz` to `barBaz`", + // } + // ], + r#"var { foo: barBaz = 1 } = quz;"#: [// new + { + col: 11, + message: "Identifier 'barBaz' is not in rust style.", + hint: "Consider renaming `barBaz` to `bar_baz`", + } + ], + // r#"const f = function no_camelcased() {};"#: [ + // { + // col: 19, + // message: "Identifier 'no_camelcased' is not in rust style.", + // hint: "Consider renaming `no_camelcased` to `noCamelcased`", + // } + // ], + r#"const f = function camelCased() {};"#: [// new + { + col: 19, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const c = class no_camelcased {};"#: [ + { + col: 16, + message: "Identifier 'no_camelcased' is not in rust style.", + hint: "Consider renaming `no_camelcased` to `NoCamelcased`", + } + ], + r#"class no_camelcased {}"#: [ + { + col: 6, + message: "Identifier 'no_camelcased' is not in rust style.", + hint: "Consider renaming `no_camelcased` to `NoCamelcased`", + } + ], + r#"type foo_bar = string;"#: [ + { + col: 5, + message: "Identifier 'foo_bar' is not in rust style.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + // r#"type Foo = { snake_case: number; };"#: [ + // { + // col: 13, + // message: "Identifier 'snake_case' is not in rust style.", + // hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, + // } + // ], + r#"type Foo = { camelCase: number; };"#: [// new + { + col: 13, + message: "Identifier 'camelCase' is not in rust style.", + hint: r#"Consider renaming `camelCase` to `camel_case`, or wrapping it in quotation mark like `"camelCase"`"#, + } + ], + r#"interface foo_bar { ok: string; };"#: [ + { + col: 10, + message: "Identifier 'foo_bar' is not in rust style.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + // r#"interface Foo { snake_case: number; };"#: [ + // { + // col: 16, + // message: "Identifier 'snake_case' is not in rust style.", + // hint: r#"Consider renaming `snake_case` to `snakeCase`, or wrapping it in quotation mark like `"snake_case"`"#, + // } + // ], + r#"interface Foo { camelCase: number; };"#: [// new + { + col: 16, + message: "Identifier 'camelCase' is not in rust style.", + hint: r#"Consider renaming `camelCase` to `camel_case`, or wrapping it in quotation mark like `"camelCase"`"#, + } + ], + r#"namespace foo_bar {}"#: [ + { + col: 10, + message: "Identifier 'foo_bar' is not in rust style.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + // r#"namespace FooBar { const snake_case = 42; }"#: [ + // { + // col: 25, + // message: "Identifier 'snake_case' is not in rust style.", + // hint: "Consider renaming `snake_case` to `snakeCase`", + // } + // ], + r#"namespace FooBar { const camelCase = 42; }"#: [// new + { + col: 25, + message: "Identifier 'camelCase' is not in rust style.", + hint: "Consider renaming `camelCase` to `camel_case`", + } + ], + r#"enum foo_bar { VariantOne }"#: [ + { + col: 5, + message: "Identifier 'foo_bar' is not in rust style.", + hint: "Consider renaming `foo_bar` to `FooBar`", + } + ], + r#"enum FooBar { variant_one }"#: [ + { + col: 14, + message: "Identifier 'variant_one' is not in rust style.", + hint: "Consider renaming `variant_one` to `VariantOne`", + } + ], + //new invalid test cases: + r#"firstName = "Ichigo""#: [ + { + col: 0, + + message: "Identifier 'firstName' is not in rust style.", + hint: "Consider renaming `firstName` to `first_name`", + } + ], + r#"__myPrivateVariable = "Hoshimiya""#: [ + { + col: 0, + message: "Identifier '__myPrivateVariable' is not in rust style.", + hint: "Consider renaming `__myPrivateVariable` to `__my_private_variable`", + } + ], + r#"myPrivateVariable_ = "Hoshimiya""#: [ + { + col: 0, + message: "Identifier 'myPrivateVariable_' is not in rust style.", + hint: "Consider renaming `myPrivateVariable_` to `my_private_variable_`", + } + ], + r#"function doSomething(){}"#: [ + { + col: 9, + message: "Identifier 'doSomething' is not in rust style.", + hint: "Consider renaming `doSomething` to `do_something`", + } + ], + r#"import { no_camelcased as camelCased } from "external-module";"#: [ + { + col: 26, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"import { no_camelcased as camelCased, anotherCamelCased } from "external-module";"#: [ + { + col: 26, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"var _camelCased = aGlobalVariable"#: [ + { + col: 4, + message: "Identifier '_camelCased' is not in rust style.", + hint: "Consider renaming `_camelCased` to `_camel_cased`", + } + ], + r#"var camelCased = _aGlobalVariable"#: [ + { + col: 4, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"function foo({ no_camelcased: camelCased }) {};"#: [ + { + col: 30, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"function foo({ camelCased = 'default value' }) {};"#: [ + { + col: 15, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"function foo({ camelCased }) {};"#: [ + { + col: 15, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const f = function camelCased() {};"#: [ + { + col: 19, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const c = class camelCased {};"#: [ + { + col: 16, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `CamelCased`", + } + ], + r#"const c = class snake_cased {};"#: [ + { + col: 16, + message: "Identifier 'snake_cased' is not in rust style.", + hint: "Consider renaming `snake_cased` to `SnakeCased`", + } + ], + r#"class snake_cased {};"#:[ + { + col: 6, + message: "Identifier 'snake_cased' is not in rust style.", + hint: "Consider renaming `snake_cased` to `SnakeCased`", + } + ], + r#"class camelCased {};"#:[ + { + col: 6, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `CamelCased`", + } + ], + r#"var { [category_id]: categoryId } = query;"#: [ + { + col: 21, + message: "Identifier 'categoryId' is not in rust style.", + hint: "Consider renaming `categoryId` to `category_id`", + } + ], + r#"var camelCased = snake_cased"#: [ + { + col: 4, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], }; } } diff --git a/www/static/docs.json b/www/static/docs.json index 4e9a743d2..ae9386832 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -642,6 +642,11 @@ "recommended" ] }, + { + "code": "rust_style", + "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces\\\nand in snake_case for functions, methods, variables.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\nconst { lastName } = obj1;\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nimport { camelCased } from \"external-module.js\";\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", + "tags": [] + }, { "code": "single-var-declarator", "docs": "Disallows multiple variable definitions in the same declaration statement\n\n### Invalid:\n\n```typescript\nconst foo = 1, bar = \"2\";\n```\n\n### Valid:\n\n```typescript\nconst foo = 1;\nconst bar = \"2\";\n```\n", From 11f4b5c1e712b2a10e380e91074a7eef14183709 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Sat, 14 Sep 2024 23:14:25 +0200 Subject: [PATCH 3/7] feat(rust_style): allow SCREAMING_SNAKE_CASE for static props and constants --- docs/rules/rust_style.md | 5 +- src/rules/rust_style.rs | 179 +++++++++++++++++++++++++++------------ www/static/docs.json | 2 +- 3 files changed, 130 insertions(+), 56 deletions(-) diff --git a/docs/rules/rust_style.md b/docs/rules/rust_style.md index 3e458cd96..8efa16c26 100644 --- a/docs/rules/rust_style.md +++ b/docs/rules/rust_style.md @@ -5,8 +5,9 @@ Consistency in a code base is key for readability and maintainability. This rule is useful for deno projects that call rust functions via FFI. It attempts to unify naming conventions and enforces declarations and object property names which you create to be\ -in UpperCamelCase/PascalCase for classes, types, interfaces\ -and in snake_case for functions, methods, variables. +in UpperCamelCase/PascalCase for classes, types, interfaces,\ +in snake_case for functions, methods, variables\ +and in SCREAMING_SNAKE_CASE for static class properties and constants. Of note: diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs index 3e06e3c35..37ecd34c3 100644 --- a/src/rules/rust_style.rs +++ b/src/rules/rust_style.rs @@ -95,18 +95,10 @@ fn to_camelcase(ident_name: &str) -> String { ident_name.to_ascii_uppercase() } -/// Convert the name of identifier into rust style. If the name is originally in rust style, return -/// the name as it is. If name starts with uppercase letter, return as is. For more detail, see the test cases below. -fn to_rust_style(ident_name: &str) -> String { - let trimmed_ident = ident_name.trim_matches('_'); - if let Some(first_char) = trimmed_ident.chars().next() { - if first_char.is_uppercase() { - return ident_name.to_string(); - } - } - +/// Convert the name of identifier into snake case. For more detail, see the test cases below. +fn to_snake_case(ident_name: &str) -> String { static LOWERCASE_UPPERCASE_CHAR_RE: Lazy = - Lazy::new(|| Regex::new(r"([^A-Z])([A-Z])").unwrap()); + Lazy::new(|| Regex::new(r"([a-z])([A-Z])").unwrap()); let result = LOWERCASE_UPPERCASE_CHAR_RE .replace_all(ident_name, |caps: &Captures| { @@ -114,15 +106,16 @@ fn to_rust_style(ident_name: &str) -> String { }); if result != ident_name { - return result.into_owned(); + return result.into_owned().to_ascii_lowercase(); } - ident_name.to_string() + ident_name.to_ascii_lowercase() } enum IdentToCheck { /// Normal variable name e.g. `foo` in `const foo = 42;` Variable(String), + VariableOrConstant(String), /// Object key name, for example: /// /// ```typescript @@ -189,6 +182,10 @@ impl IdentToCheck { Self::Variable(name.as_ref().to_string()) } + fn variable_or_constant(name: impl AsRef) -> Self { + Self::VariableOrConstant(name.as_ref().to_string()) + } + fn object_key(key_name: impl AsRef, is_shorthand: bool) -> Self { Self::ObjectKey { key_name: key_name.as_ref().to_string(), @@ -261,6 +258,7 @@ impl IdentToCheck { match self { IdentToCheck::Variable(name) | IdentToCheck::Function(name) + | IdentToCheck::VariableOrConstant(name) | IdentToCheck::Class(name) | IdentToCheck::TypeAlias(name) | IdentToCheck::Interface(name) @@ -294,7 +292,16 @@ impl IdentToCheck { fn to_hint(&self) -> String { match self { IdentToCheck::Variable(name) | IdentToCheck::Function(name) => { - format!("Consider renaming `{}` to `{}`", name, to_rust_style(name)) + format!("Consider renaming `{}` to `{}`", name, to_snake_case(name)) + } + IdentToCheck::VariableOrConstant(name) => { + let snake_cased = to_snake_case(name); + format!( + "Consider renaming `{}` to `{}` or `{}`", + name, + snake_cased, + snake_cased.to_ascii_uppercase() + ) } IdentToCheck::ObjectKey { ref key_name, @@ -302,14 +309,14 @@ impl IdentToCheck { } => { if *is_shorthand { format!( - r#"Consider writing `{rust_styled}: {original}` or `"{original}": {original}`"#, - rust_styled = to_rust_style(key_name), + r#"Consider writing `{snake_cased}: {original}` or `"{original}": {original}`"#, + snake_cased = to_snake_case(key_name), original = key_name ) } else { format!( - r#"Consider renaming `{original}` to `{rust_styled}`, or wrapping it in quotation mark like `"{original}"`"#, - rust_styled = to_rust_style(key_name), + r#"Consider renaming `{original}` to `{snake_cased}`, or wrapping it in quotation mark like `"{original}"`"#, + snake_cased = to_snake_case(key_name), original = key_name ) } @@ -356,7 +363,7 @@ impl IdentToCheck { return format!( "Consider renaming `{}` to `{}`", name, - to_rust_style(name), + to_snake_case(name), ); } @@ -364,14 +371,14 @@ impl IdentToCheck { return format!( "Consider replacing `{{ {key} = .. }}` with `{{ {key}: {value} = .. }}`", key = key_name, - value = to_rust_style(key_name), + value = to_snake_case(key_name), ); } format!( "Consider replacing `{{ {key} }}` with `{{ {key}: {value} }}`", key = key_name, - value = to_rust_style(key_name), + value = to_snake_case(key_name), ) } IdentToCheck::NamedImport { local, imported } => { @@ -379,13 +386,13 @@ impl IdentToCheck { format!( "Consider renaming `{}` to `{}`", local, - to_rust_style(local) + to_snake_case(local) ) } else { format!( - "Consider replacing `{{ {local} }}` with `{{ {local} as {rust_styled_local} }}`", + "Consider replacing `{{ {local} }}` with `{{ {local} as {snake_cased_local} }}`", local = local, - rust_styled_local = to_rust_style(local), + snake_cased_local = to_snake_case(local), ) } } @@ -525,21 +532,36 @@ impl RustStyleHandler { } } - fn check_pat(&mut self, pat: &ast_view::Pat) { + fn check_pat(&mut self, pat: &ast_view::Pat, ctx: &mut Context) { match pat { ast_view::Pat::Ident(ident) => { - self.check_ident_snake_cased( - ident, - IdentToCheck::variable(ident.id.inner), - ); + //todo: ?ctx always valid? + if let Some(v) = ctx.scope().var_by_ident(ident.id) { + if let deno_ast::BindingKind::Const = v.kind() { + self.check_ident_snake_cased_or_screaming_snake_cased( + ident, + IdentToCheck::variable_or_constant(ident.id.inner), + ); + } else { + self.check_ident_snake_cased( + ident, + IdentToCheck::variable(ident.id.inner), + ); + } + } else { + self.check_ident_snake_cased( + ident, + IdentToCheck::variable(ident.id.inner), + ); + } } ast_view::Pat::Array(ast_view::ArrayPat { elems, .. }) => { for pat in elems.iter().flatten() { - self.check_pat(pat); + self.check_pat(pat, ctx); } } ast_view::Pat::Rest(ast_view::RestPat { ref arg, .. }) => { - self.check_pat(arg); + self.check_pat(arg, ctx); } ast_view::Pat::Object(ast_view::ObjectPat { props, .. }) => { for prop in *props { @@ -575,7 +597,7 @@ impl RustStyleHandler { ); } _ => { - self.check_pat(value); + self.check_pat(value, ctx); } }, ast_view::ObjectPatProp::Assign(ast_view::AssignPatProp { @@ -601,13 +623,13 @@ impl RustStyleHandler { ref arg, .. }) => { - self.check_pat(arg); + self.check_pat(arg, ctx); } } } } ast_view::Pat::Assign(ast_view::AssignPat { ref left, .. }) => { - self.check_pat(left); + self.check_pat(left, ctx); } ast_view::Pat::Expr(expr) => { if let ast_view::Expr::Ident(ident) = expr { @@ -629,11 +651,26 @@ impl Handler for RustStyleHandler { for ident in idents { self.check_ident_snake_cased_or_screaming_snake_cased( &ident.range(), - IdentToCheck::variable(ident.to_id().0), + IdentToCheck::variable_or_constant(ident.to_id().0), ); } } + fn class_prop( + &mut self, + class_prop: &ast_view::ClassProp, + _ctx: &mut Context, + ) { + if class_prop.is_static() { + if let ast_view::PropName::Ident(ident) = class_prop.key { + self.check_ident_snake_cased_or_screaming_snake_cased( + ident, + IdentToCheck::variable_or_constant(ident.inner), + ); + } + } + } + fn fn_decl(&mut self, fn_decl: &ast_view::FnDecl, ctx: &mut Context) { if fn_decl.declare() { ctx.stop_traverse(); @@ -669,7 +706,7 @@ impl Handler for RustStyleHandler { } for decl in var_decl.decls { - self.check_pat(&decl.name); + self.check_pat(&decl.name, ctx); if let Some(expr) = &decl.init { match expr { @@ -733,8 +770,8 @@ impl Handler for RustStyleHandler { } } - fn param(&mut self, param: &ast_view::Param, _ctx: &mut Context) { - self.check_pat(¶m.pat); + fn param(&mut self, param: &ast_view::Param, ctx: &mut Context) { + self.check_pat(¶m.pat, ctx); } fn import_named_specifier( @@ -982,7 +1019,7 @@ mod tests { ("__fooBar_baz__", false), //not snake ("__fooBarBaz__", false), //camel ("Sha3_224", false), //not snake - ("SHA3_224", true), //screaming snake; todo: should this be true? + ("SHA3_224", true), //screaming snake; todo: ?should this be true? ]; for &(input, expected) in tests.iter() { @@ -1012,24 +1049,24 @@ mod tests { } #[test] - fn test_to_rust_style() { + fn test_to_snake_case() { let tests = [ ("fooBar", "foo_bar"), ("foo_bar", "foo_bar"), - ("FooBar", "FooBar"), - ("_FooBar", "_FooBar"), + ("FooBar", "foo_bar"), + ("_FooBar", "_foo_bar"), ("fooBarBaz", "foo_bar_baz"), ("_fooBarBaz", "_foo_bar_baz"), ("__fooBarBaz__", "__foo_bar_baz__"), - ("Sha3_224", "Sha3_224"), - ("SHA3_224", "SHA3_224"), - ("_leading", "_leading"), - ("trailing_", "trailing_"), - ("_bothends_", "_bothends_"), + ("Sha3_224", "sha3_224"), + ("SHA3_224", "sha3_224"), + ("_leadingCamel", "_leading_camel"), + ("trailingCamel_", "trailing_camel_"), + ("_bothendsCamel_", "_bothends_camel_"), ]; for &(input, expected) in tests.iter() { - assert_eq!(expected, to_rust_style(input)); + assert_eq!(expected, to_snake_case(input)); } } @@ -1048,6 +1085,10 @@ mod tests { IdentToCheck::Function(s("fooBar")), "Consider renaming `fooBar` to `foo_bar`", ), + ( + IdentToCheck::VariableOrConstant(s("fooBar")), + "Consider renaming `fooBar` to `foo_bar` or `FOO_BAR`", + ), ( IdentToCheck::Class(s("foo_bar")), "Consider renaming `foo_bar` to `FooBar`", @@ -1140,7 +1181,11 @@ mod tests { r#"new DoSomething()"#,// new r#"foo.do_something()"#, r#"var foo = bar.baz_boom;"#, + r#"var foo = bar.BAZ_BOOM;"#,// new + r#"var foo = bar.bazBoom;"#,// new r#"var foo = bar.baz_boom.something;"#, + r#"var foo = bar.BAZ_BOOM.something;"#,// new + r#"var foo = bar.bazBoom.something;"#,// new r#"foo.boom_pow.qux = bar.baz_boom.something;"#, r#"if (bar.baz_boom) {}"#, r#"var obj = { key: foo.bar_baz };"#, @@ -1293,6 +1338,12 @@ mod tests { r#"type Foo = { snake_case: number; };"#, r#"interface Foo { snake_case: number; };"#, r#"namespace FooBar { const snake_case = 42; }"#, + + //new variable or constant test cases: + r#"const snake_case = 42;"#, + r#"const SCREAMING_SNAKE_CASE = 42;"#, + r#"class FooBar { static snake_case = 42; }"#, + r#"class FooBar { static SCREAMING_SNAKE_CASE = 42; }"#, }; } @@ -1533,7 +1584,7 @@ mod tests { { col: 6, message: "Identifier 'camelCased' is not in rust style.", - hint: "Consider renaming `camelCased` to `camel_cased`", + hint: "Consider renaming `camelCased` to `camel_cased` or `CAMEL_CASED`", }, { col: 37, @@ -1699,7 +1750,7 @@ mod tests { { col: 25, message: "Identifier 'camelCase' is not in rust style.", - hint: "Consider renaming `camelCase` to `camel_case`", + hint: "Consider renaming `camelCase` to `camel_case` or `CAMEL_CASE`", } ], r#"enum foo_bar { VariantOne }"#: [ @@ -1722,21 +1773,21 @@ mod tests { col: 0, message: "Identifier 'firstName' is not in rust style.", - hint: "Consider renaming `firstName` to `first_name`", + hint: "Consider renaming `firstName` to `first_name` or `FIRST_NAME`", } ], r#"__myPrivateVariable = "Hoshimiya""#: [ { col: 0, message: "Identifier '__myPrivateVariable' is not in rust style.", - hint: "Consider renaming `__myPrivateVariable` to `__my_private_variable`", + hint: "Consider renaming `__myPrivateVariable` to `__my_private_variable` or `__MY_PRIVATE_VARIABLE`", } ], r#"myPrivateVariable_ = "Hoshimiya""#: [ { col: 0, message: "Identifier 'myPrivateVariable_' is not in rust style.", - hint: "Consider renaming `myPrivateVariable_` to `my_private_variable_`", + hint: "Consider renaming `myPrivateVariable_` to `my_private_variable_` or `MY_PRIVATE_VARIABLE_`", } ], r#"function doSomething(){}"#: [ @@ -1844,6 +1895,28 @@ mod tests { hint: "Consider renaming `camelCased` to `camel_cased`", } ], + //new invalid variable or constant test cases: + r#"var UpperCamelCased = 42"#: [ + { + col: 4, + message: "Identifier 'UpperCamelCased' is not in rust style.", + hint: "Consider renaming `UpperCamelCased` to `upper_camel_cased`", + } + ], + r#"class FooBar { static camelCase = 42; }"#: [ + { + col: 22, + message: "Identifier 'camelCase' is not in rust style.", + hint: "Consider renaming `camelCase` to `camel_case` or `CAMEL_CASE`", + } + ], + r#"class FooBar { static UpperCamelCased = 42; }"#: [ + { + col: 22, + message: "Identifier 'UpperCamelCased' is not in rust style.", + hint: "Consider renaming `UpperCamelCased` to `upper_camel_cased` or `UPPER_CAMEL_CASED`", + } + ], }; } } diff --git a/www/static/docs.json b/www/static/docs.json index ae9386832..0bdc3a1fc 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -644,7 +644,7 @@ }, { "code": "rust_style", - "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces\\\nand in snake_case for functions, methods, variables.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\nconst { lastName } = obj1;\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nimport { camelCased } from \"external-module.js\";\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", + "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces,\\\nin snake_case for functions, methods, variables\\\nand in SCREAMING_SNAKE_CASE for static class properties and constants.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\nconst { lastName } = obj1;\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nimport { camelCased } from \"external-module.js\";\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", "tags": [] }, { From fa9e8bb904a9ba7ea32a91cacafb544b5bca44a0 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Sun, 15 Sep 2024 14:52:24 +0200 Subject: [PATCH 4/7] fix(rust_style): added doc test cases and discovered #1187 --- docs/rules/rust_style.md | 4 +- src/rules/rust_style.rs | 148 +++++++++++++++++++++++++++++++++++++++ www/static/docs.json | 2 +- 3 files changed, 151 insertions(+), 3 deletions(-) diff --git a/docs/rules/rust_style.md b/docs/rules/rust_style.md index 8efa16c26..d4cbfdaec 100644 --- a/docs/rules/rust_style.md +++ b/docs/rules/rust_style.md @@ -24,7 +24,6 @@ Of note: let firstName = "Ichigo"; const obj1 = { lastName: "Hoshimiya" }; const obj2 = { firstName }; -const { lastName } = obj1; function doSomething() {} function foo({ camelCase = "default value" }) {} @@ -33,7 +32,6 @@ class snake_case_class {} class camelCaseClass {} class Also_Not_Valid_Class {} -import { camelCased } from "external-module.js"; export * as camelCased from "mod.ts"; enum snake_case_enum { @@ -66,6 +64,7 @@ const __my_private_variable = "Hoshimiya"; const my_private_variable_ = "Hoshimiya"; const obj1 = { "lastName": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid const obj2 = { "firstName": firstName }; +const { lastName } = obj1; //valid, because one has no control over the identifier const { lastName: last_name } = obj; function do_something() {} // function declarations must be snake_case but... @@ -74,6 +73,7 @@ function foo({ camelCase: snake_case = "default value" }) {} class PascalCaseClass {} +import { camelCased } from "external-module.js"; //valid, because one has no control over the identifier import { camelCased as not_camel_cased } from "external-module.js"; export * as not_camel_cased from "mod.ts"; diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs index 37ecd34c3..20a43f339 100644 --- a/src/rules/rust_style.rs +++ b/src/rules/rust_style.rs @@ -1344,6 +1344,28 @@ mod tests { r#"const SCREAMING_SNAKE_CASE = 42;"#, r#"class FooBar { static snake_case = 42; }"#, r#"class FooBar { static SCREAMING_SNAKE_CASE = 42; }"#, + + r#"class STILL_VALID_CLASS {}"#,// considered UpperCamelCased + + //doc test cases: + r#"let first_name = "Ichigo";"#, + r#"const FIRST_NAME = "Ichigo";"#, + r#"const __my_private_variable = "Hoshimiya";"#, + r#"const my_private_variable_ = "Hoshimiya";"#, + r#"const obj1 = { "lastName": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid"#, + r#"const obj2 = { "firstName": firstName };"#, + r#"const { lastName } = obj1; //valid, because one has no control over the identifier"#,// valid, see #1187 + r#"const { lastName: last_name } = obj;"#, + r#"function do_something() {} // function declarations must be snake_case but..."#, + r#"doSomething(); // ...camel_case function calls are allowed"#, + r#"function foo({ camelCase: snake_case = "default value" }) {}"#, + r#"class PascalCaseClass {}"#, + r#"import { camelCased } from "external-module.js"; //valid, because one has no control over the identifier"#,// valid, see #1187 + r#"import { camelCased as not_camel_cased } from "external-module.js";"#, + r#"export * as not_camel_cased from "mod.ts";"#, + r#"enum PascalCaseEnum { PascalCaseVariant }"#, + r#"type PascalCaseType = { some_property: number };"#, + r#"interface PascalCaseInterface { some_property: number; }"#, }; } @@ -1917,6 +1939,132 @@ mod tests { hint: "Consider renaming `UpperCamelCased` to `upper_camel_cased` or `UPPER_CAMEL_CASED`", } ], + //doc test cases: + r#"let firstName = "Ichigo";"#: [ + { + col: 4, + message: "Identifier 'firstName' is not in rust style.", + hint: "Consider renaming `firstName` to `first_name`", + }, + ], + r#"const obj1 = { lastName: "Hoshimiya" };"#: [ + { + col: 15, + message: "Identifier 'lastName' is not in rust style.", + hint: "Consider renaming `lastName` to `last_name`, or wrapping it in quotation mark like `\"lastName\"`", + }, + ], + r#"const obj2 = { firstName };"#: [ + { + col: 15, + message: "Identifier 'firstName' is not in rust style.", + hint: "Consider writing `first_name: firstName` or `\"firstName\": firstName`", + }, + ], + r#"function doSomething() {}"#: [ + { + col: 9, + message: "Identifier 'doSomething' is not in rust style.", + hint: "Consider renaming `doSomething` to `do_something`", + }, + ], + r#"function foo({ camelCase = "default value" }) {}"#: [ + { + col: 15, + message: "Identifier 'camelCase' is not in rust style.", + hint: "Consider renaming `camelCase` to `camel_case`", + }, + ], + r#"class snake_case_class {}"#: [ + { + col: 6, + message: "Identifier 'snake_case_class' is not in rust style.", + hint: "Consider renaming `snake_case_class` to `SnakeCaseClass`", + }, + ], + r#"class camelCaseClass {}"#: [ + { + col: 6, + message: "Identifier 'camelCaseClass' is not in rust style.", + hint: "Consider renaming `camelCaseClass` to `CamelCaseClass`", + }, + ], + r#"class Also_Not_Valid_Class {}"#: [ + { + col: 6, + message: "Identifier 'Also_Not_Valid_Class' is not in rust style.", + hint: "Consider renaming `Also_Not_Valid_Class` to `ALSO_NOT_VALID_CLASS`", + }, + ], + r#"export * as camelCased from "mod.ts";"#: [ + { + col: 12, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + }, + ], + r#"enum snake_case_enum { snake_case_variant }"#: [ + { + col: 5, + message: "Identifier 'snake_case_enum' is not in rust style.", + hint: "Consider renaming `snake_case_enum` to `SnakeCaseEnum`", + }, + { + col: 23, + message: "Identifier 'snake_case_variant' is not in rust style.", + hint: "Consider renaming `snake_case_variant` to `SnakeCaseVariant`", + }, + ], + r#"enum camelCasedEnum { camelCasedVariant }"#: [ + { + col: 5, + message: "Identifier 'camelCasedEnum' is not in rust style.", + hint: "Consider renaming `camelCasedEnum` to `CamelCasedEnum`", + }, + { + col: 22, + message: "Identifier 'camelCasedVariant' is not in rust style.", + hint: "Consider renaming `camelCasedVariant` to `CamelCasedVariant`", + }, + ], + r#"type snake_case_type = { some_property: number };"#: [ + { + col: 5, + message: "Identifier 'snake_case_type' is not in rust style.", + hint: "Consider renaming `snake_case_type` to `SnakeCaseType`", + }, + ], + r#"type camelCasedType = { someProperty: number };"#: [ + { + col: 5, + message: "Identifier 'camelCasedType' is not in rust style.", + hint: "Consider renaming `camelCasedType` to `CamelCasedType`", + }, + { + col: 24, + message: "Identifier 'someProperty' is not in rust style.", + hint: "Consider renaming `someProperty` to `some_property`, or wrapping it in quotation mark like `\"someProperty\"`", + }, + ], + r#"interface snake_case_interface { some_property: number; }"#: [ + { + col: 10, + message: "Identifier 'snake_case_interface' is not in rust style.", + hint: "Consider renaming `snake_case_interface` to `SnakeCaseInterface`", + }, + ], + r#"interface camelCasedInterface { someProperty: number; }"#: [ + { + col: 10, + message: "Identifier 'camelCasedInterface' is not in rust style.", + hint: "Consider renaming `camelCasedInterface` to `CamelCasedInterface`", + }, + { + col: 32, + message: "Identifier 'someProperty' is not in rust style.", + hint: "Consider renaming `someProperty` to `some_property`, or wrapping it in quotation mark like `\"someProperty\"`", + }, + ], }; } } diff --git a/www/static/docs.json b/www/static/docs.json index 0bdc3a1fc..736030d08 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -644,7 +644,7 @@ }, { "code": "rust_style", - "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces,\\\nin snake_case for functions, methods, variables\\\nand in SCREAMING_SNAKE_CASE for static class properties and constants.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\nconst { lastName } = obj1;\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nimport { camelCased } from \"external-module.js\";\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", + "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces,\\\nin snake_case for functions, methods, variables\\\nand in SCREAMING_SNAKE_CASE for static class properties and constants.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName } = obj1; //valid, because one has no control over the identifier\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased } from \"external-module.js\"; //valid, because one has no control over the identifier\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", "tags": [] }, { From e1fa0aabfb88d394028517ad384b6662d995a825 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:21:45 +0200 Subject: [PATCH 5/7] feat(rust_style): consistency --- docs/rules/rust_style.md | 4 ++-- www/static/docs.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rules/rust_style.md b/docs/rules/rust_style.md index d4cbfdaec..11afd6fcd 100644 --- a/docs/rules/rust_style.md +++ b/docs/rules/rust_style.md @@ -64,7 +64,7 @@ const __my_private_variable = "Hoshimiya"; const my_private_variable_ = "Hoshimiya"; const obj1 = { "lastName": "Hoshimiya" }; // if an object key is wrapped in quotation mark, then it's valid const obj2 = { "firstName": firstName }; -const { lastName } = obj1; //valid, because one has no control over the identifier +const { lastName } = obj1; // valid, because one has no control over the identifier const { lastName: last_name } = obj; function do_something() {} // function declarations must be snake_case but... @@ -73,7 +73,7 @@ function foo({ camelCase: snake_case = "default value" }) {} class PascalCaseClass {} -import { camelCased } from "external-module.js"; //valid, because one has no control over the identifier +import { camelCased } from "external-module.js"; // valid, because one has no control over the identifier import { camelCased as not_camel_cased } from "external-module.js"; export * as not_camel_cased from "mod.ts"; diff --git a/www/static/docs.json b/www/static/docs.json index 736030d08..b947ceea3 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -644,7 +644,7 @@ }, { "code": "rust_style", - "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces,\\\nin snake_case for functions, methods, variables\\\nand in SCREAMING_SNAKE_CASE for static class properties and constants.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName } = obj1; //valid, because one has no control over the identifier\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased } from \"external-module.js\"; //valid, because one has no control over the identifier\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", + "docs": "Enforces the use of Rust-style naming conventions, see\nhttps://rust-lang.github.io/api-guidelines/naming.html\n\nConsistency in a code base is key for readability and maintainability. This rule\nis useful for deno projects that call rust functions via FFI. It attempts to\nunify naming conventions and enforces declarations and object property names\nwhich you create to be\\\nin UpperCamelCase/PascalCase for classes, types, interfaces,\\\nin snake_case for functions, methods, variables\\\nand in SCREAMING_SNAKE_CASE for static class properties and constants.\n\nOf note:\n\n- `_` is allowed at the start or end of a variable\n- All uppercase variable names (e.g. constants) may have `_` in their name\n- If you have to use a camelCase key in an object for some reasons, wrap it in\n quotation mark\n- This rule also applies to variables imported or exported via ES modules, but\n not to object properties of those variables\n\n### Invalid:\n\n```typescript\nlet firstName = \"Ichigo\";\nconst obj1 = { lastName: \"Hoshimiya\" };\nconst obj2 = { firstName };\n\nfunction doSomething() {}\nfunction foo({ camelCase = \"default value\" }) {}\n\nclass snake_case_class {}\nclass camelCaseClass {}\nclass Also_Not_Valid_Class {}\n\nexport * as camelCased from \"mod.ts\";\n\nenum snake_case_enum {\n snake_case_variant,\n}\n\nenum camelCasedEnum {\n camelCasedVariant,\n}\n\ntype snake_case_type = { some_property: number };\n\ntype camelCasedType = { someProperty: number };\n\ninterface snake_case_interface {\n some_property: number;\n}\n\ninterface camelCasedInterface {\n someProperty: number;\n}\n```\n\n### Valid:\n\n```typescript\nlet first_name = \"Ichigo\";\nconst FIRST_NAME = \"Ichigo\";\nconst __my_private_variable = \"Hoshimiya\";\nconst my_private_variable_ = \"Hoshimiya\";\nconst obj1 = { \"lastName\": \"Hoshimiya\" }; // if an object key is wrapped in quotation mark, then it's valid\nconst obj2 = { \"firstName\": firstName };\nconst { lastName } = obj1; // valid, because one has no control over the identifier\nconst { lastName: last_name } = obj;\n\nfunction do_something() {} // function declarations must be snake_case but...\ndoSomething(); // ...camel_case function calls are allowed\nfunction foo({ camelCase: snake_case = \"default value\" }) {}\n\nclass PascalCaseClass {}\n\nimport { camelCased } from \"external-module.js\"; // valid, because one has no control over the identifier\nimport { camelCased as not_camel_cased } from \"external-module.js\";\nexport * as not_camel_cased from \"mod.ts\";\n\nenum PascalCaseEnum {\n PascalCaseVariant,\n}\n\ntype PascalCaseType = { some_property: number };\n\ninterface PascalCaseInterface {\n some_property: number;\n}\n```\n", "tags": [] }, { From 3e669872309a48cde2f74cb702f7c0e9c56fa95a Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:38:43 +0200 Subject: [PATCH 6/7] feat(rust_style): allow UpperCamelCase function JSX components --- src/rules/rust_style.rs | 130 +++++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 21 deletions(-) diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs index 20a43f339..694b807d6 100644 --- a/src/rules/rust_style.rs +++ b/src/rules/rust_style.rs @@ -76,7 +76,7 @@ fn is_upper_camel_cased(ident_name: &str) -> bool { /// Convert the name of identifier into camel case. If the name is originally in camel case, return /// the name as it is. For more detail, see the test cases below. -fn to_camelcase(ident_name: &str) -> String { +fn to_camel_case(ident_name: &str) -> String { if !is_underscored(ident_name) { return ident_name.to_string(); } @@ -95,6 +95,21 @@ fn to_camelcase(ident_name: &str) -> String { ident_name.to_ascii_uppercase() } +fn to_upper_camel_case(ident_name: &str) -> String { + let camel_cased = to_camel_case(ident_name); + static FIRST_CHAR_LOWERCASE: Lazy = + Lazy::new(|| Regex::new(r"^_*[a-z]").unwrap()); + + let result = FIRST_CHAR_LOWERCASE + .replace(&camel_cased, |caps: &Captures| caps[0].to_ascii_uppercase()); + + if result != ident_name { + return result.into_owned(); + } + + ident_name.to_string() +} + /// Convert the name of identifier into snake case. For more detail, see the test cases below. fn to_snake_case(ident_name: &str) -> String { static LOWERCASE_UPPERCASE_CHAR_RE: Lazy = @@ -129,6 +144,8 @@ enum IdentToCheck { }, /// Function name e.g. `foo` in `function foo() {}` Function(String), + /// Function name e.g. `foo` in `function foo() {}` or `Foo` in `function Foo() {}` + FunctionOrComponent(String), /// Class name e.g. `Foo` in `class Foo {}` Class(String), /// Type alias e.g. `Foo` in `type Foo = string;` @@ -197,6 +214,10 @@ impl IdentToCheck { Self::Function(name.as_ref().to_string()) } + fn function_or_component(name: impl AsRef) -> Self { + Self::FunctionOrComponent(name.as_ref().to_string()) + } + fn class(name: impl AsRef) -> Self { Self::Class(name.as_ref().to_string()) } @@ -258,6 +279,7 @@ impl IdentToCheck { match self { IdentToCheck::Variable(name) | IdentToCheck::Function(name) + | IdentToCheck::FunctionOrComponent(name) | IdentToCheck::VariableOrConstant(name) | IdentToCheck::Class(name) | IdentToCheck::TypeAlias(name) @@ -294,6 +316,14 @@ impl IdentToCheck { IdentToCheck::Variable(name) | IdentToCheck::Function(name) => { format!("Consider renaming `{}` to `{}`", name, to_snake_case(name)) } + IdentToCheck::FunctionOrComponent(name) => { + format!( + "Consider renaming `{}` to `{}` or `{}`", + name, + to_snake_case(name), + to_upper_camel_case(name) + ) + } IdentToCheck::VariableOrConstant(name) => { let snake_cased = to_snake_case(name); format!( @@ -328,10 +358,6 @@ impl IdentToCheck { | IdentToCheck::EnumVariant(name) | IdentToCheck::Namespace(name) | IdentToCheck::Module(name) => { - let camel_cased = to_camelcase(name); - static FIRST_CHAR_LOWERCASE: Lazy = - Lazy::new(|| Regex::new(r"^[a-z]").unwrap()); - // The following names should be in pascal case // - class // - type alias @@ -340,11 +366,11 @@ impl IdentToCheck { // - enum variant // - namespace // - module - let pascal_cased = FIRST_CHAR_LOWERCASE - .replace(&camel_cased, |caps: &Captures| { - caps[0].to_ascii_uppercase() - }); - format!("Consider renaming `{}` to `{}`", name, pascal_cased) + format!( + "Consider renaming `{}` to `{}`", + name, + to_upper_camel_case(name) + ) } IdentToCheck::ObjectPat { key_name, @@ -474,6 +500,21 @@ impl RustStyleHandler { } } + /// Check if this ident is snake cased or screaming snake cased only when it's not yet visited. + fn check_ident_snake_cased_or_upper_camel_cased( + &mut self, + range: &S, + ident: IdentToCheck, + ) { + let range = range.range(); + if self.visited.insert(range) + && !is_snake_cased(ident.get_ident_name()) + && !is_upper_camel_cased(ident.get_ident_name()) + { + self.errors.insert(range, ident); + } + } + fn check_ts_type(&mut self, ty: &ast_view::TsType) { if let ast_view::TsType::TsTypeLit(type_lit) = ty { for member in type_lit.members { @@ -538,10 +579,27 @@ impl RustStyleHandler { //todo: ?ctx always valid? if let Some(v) = ctx.scope().var_by_ident(ident.id) { if let deno_ast::BindingKind::Const = v.kind() { - self.check_ident_snake_cased_or_screaming_snake_cased( - ident, - IdentToCheck::variable_or_constant(ident.id.inner), - ); + match ident.parent() { + ast_view::Node::VarDeclarator(ast_view::VarDeclarator { + init: Some(ast_view::Expr::Fn(_)), + .. + }) + | ast_view::Node::VarDeclarator(ast_view::VarDeclarator { + init: Some(ast_view::Expr::Arrow(_)), + .. + }) => { + self.check_ident_snake_cased_or_upper_camel_cased( + ident, + IdentToCheck::function_or_component(ident.id.inner), + ); + } + _ => { + self.check_ident_snake_cased_or_screaming_snake_cased( + ident, + IdentToCheck::variable_or_constant(ident.id.inner), + ); + } + } } else { self.check_ident_snake_cased( ident, @@ -677,9 +735,9 @@ impl Handler for RustStyleHandler { return; } - self.check_ident_snake_cased( + self.check_ident_snake_cased_or_upper_camel_cased( &fn_decl.ident, - IdentToCheck::function(fn_decl.ident.inner), + IdentToCheck::function_or_component(fn_decl.ident.inner), ); } @@ -1028,7 +1086,7 @@ mod tests { } #[test] - fn test_to_camelcase() { + fn test_to_camel_case() { let tests = [ ("foo_bar", "fooBar"), ("fooBar", "fooBar"), @@ -1044,7 +1102,7 @@ mod tests { ]; for &(input, expected) in tests.iter() { - assert_eq!(expected, to_camelcase(input)); + assert_eq!(expected, to_camel_case(input)); } } @@ -1070,6 +1128,27 @@ mod tests { } } + #[test] + fn test_to_upper_camel_case() { + let tests = [ + ("foo_bar", "FooBar"), + ("fooBar", "FooBar"), + ("FooBar", "FooBar"), + ("foo_bar_baz", "FooBarBaz"), + ("_foo_bar_baz", "_FooBarBaz"), + ("__foo_bar_baz__", "__FooBarBaz__"), + ("Sha3_224", "SHA3_224"), + ("SHA3_224", "SHA3_224"), + ("_leading", "_Leading"), + ("trailing_", "Trailing_"), + ("_bothEnds_", "_BothEnds_"), + ]; + + for &(input, expected) in tests.iter() { + assert_eq!(expected, to_upper_camel_case(input)); + } + } + #[test] fn test_to_hint() { fn s(s: &str) -> String { @@ -1085,6 +1164,10 @@ mod tests { IdentToCheck::Function(s("fooBar")), "Consider renaming `fooBar` to `foo_bar`", ), + ( + IdentToCheck::FunctionOrComponent(s("fooBar")), + "Consider renaming `fooBar` to `foo_bar` or `FooBar`", + ), ( IdentToCheck::VariableOrConstant(s("fooBar")), "Consider renaming `fooBar` to `foo_bar` or `FOO_BAR`", @@ -1345,6 +1428,11 @@ mod tests { r#"class FooBar { static snake_case = 42; }"#, r#"class FooBar { static SCREAMING_SNAKE_CASE = 42; }"#, + //new function or component test cases: + r#"function ComponentTest(){}"#, + r#"const Component = function do_something() {};"#, + r#"const ComponentTest = () => {};"#, + r#"class STILL_VALID_CLASS {}"#,// considered UpperCamelCased //doc test cases: @@ -1384,7 +1472,7 @@ mod tests { { col: 9, message: "Identifier 'fooBar' is not in rust style.", - hint: "Consider renaming `fooBar` to `foo_bar`", + hint: "Consider renaming `fooBar` to `foo_bar` or `FooBar`", } ], // r#"var foo = { bar_baz: boom.bam_pow }"#: [ @@ -1816,7 +1904,7 @@ mod tests { { col: 9, message: "Identifier 'doSomething' is not in rust style.", - hint: "Consider renaming `doSomething` to `do_something`", + hint: "Consider renaming `doSomething` to `do_something` or `DoSomething`", } ], r#"import { no_camelcased as camelCased } from "external-module";"#: [ @@ -1965,7 +2053,7 @@ mod tests { { col: 9, message: "Identifier 'doSomething' is not in rust style.", - hint: "Consider renaming `doSomething` to `do_something`", + hint: "Consider renaming `doSomething` to `do_something` or `DoSomething`", }, ], r#"function foo({ camelCase = "default value" }) {}"#: [ From 08405ed3fab830e65b561943786d7e47784c2287 Mon Sep 17 00:00:00 2001 From: c-antin <38494182+c-antin@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:50:09 +0200 Subject: [PATCH 7/7] feat(rust_style): check class props + methods --- src/rules/rust_style.rs | 71 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/rules/rust_style.rs b/src/rules/rust_style.rs index 694b807d6..df519d545 100644 --- a/src/rules/rust_style.rs +++ b/src/rules/rust_style.rs @@ -719,16 +719,54 @@ impl Handler for RustStyleHandler { class_prop: &ast_view::ClassProp, _ctx: &mut Context, ) { - if class_prop.is_static() { - if let ast_view::PropName::Ident(ident) = class_prop.key { + if let ast_view::PropName::Ident(ident) = class_prop.key { + if class_prop.is_static() { self.check_ident_snake_cased_or_screaming_snake_cased( ident, IdentToCheck::variable_or_constant(ident.inner), ); + } else { + self + .check_ident_snake_cased(ident, IdentToCheck::variable(ident.inner)); } } } + fn private_prop( + &mut self, + private_prop: &ast_view::PrivateProp, + _ctx: &mut Context, + ) { + let ident = private_prop.key; + if private_prop.is_static() { + self.check_ident_snake_cased_or_screaming_snake_cased( + ident, + IdentToCheck::variable_or_constant(ident.name()), + ); + } else { + self.check_ident_snake_cased(ident, IdentToCheck::variable(ident.name())); + } + } + + fn class_method( + &mut self, + class_method: &ast_view::ClassMethod, + _ctx: &mut Context, + ) { + if let ast_view::PropName::Ident(ident) = class_method.key { + self.check_ident_snake_cased(ident, IdentToCheck::function(ident.inner)); + } + } + + fn private_method( + &mut self, + private_method: &ast_view::PrivateMethod, + _ctx: &mut Context, + ) { + let ident = private_method.key; + self.check_ident_snake_cased(ident, IdentToCheck::function(ident.name())); + } + fn fn_decl(&mut self, fn_decl: &ast_view::FnDecl, ctx: &mut Context) { if fn_decl.declare() { ctx.stop_traverse(); @@ -2027,6 +2065,35 @@ mod tests { hint: "Consider renaming `UpperCamelCased` to `upper_camel_cased` or `UPPER_CAMEL_CASED`", } ], + //new invalid class property or method test cases: + r#"const c = class { camelCased = 1; }"#: [ + { + col: 18, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const c = class { #camelCased = 1; }"#: [ + { + col: 18, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const c = class { camelCased() {}; }"#: [ + { + col: 18, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], + r#"const c = class { #camelCased() {}; }"#: [ + { + col: 18, + message: "Identifier 'camelCased' is not in rust style.", + hint: "Consider renaming `camelCased` to `camel_cased`", + } + ], //doc test cases: r#"let firstName = "Ichigo";"#: [ {