diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8321c5a948..7e87a7808678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,27 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom Contributed by @Conaclos +- [useNamingConvention](https://biomejs.dev/linter/rules/use-naming-convention) now supports [unicase](https://en.wikipedia.org/wiki/Unicase) letters ([#1786](https://github.com/biomejs/biome/issues/1786)). + + [unicase](https://en.wikipedia.org/wiki/Unicase) letters have a single case: they are neither uppercase nor lowercase. + Previously, Biome reported names in unicase as invalid. + It now accepts a name in unicase everywhere. + + The following code is now accepted: + + ```js + const 안녕하세요 = { 안녕하세요: 0 }; + ``` + + We still reject a name that mixes unicase characters with lowercase or uppercase characters: + The following names are rejected: + + ```js + const A안녕하세요 = { a안녕하세요: 0 }; + ``` + + Contributed by @Conaclos + #### Bug fixes - Fix [#1651](https://github.com/biomejs/biome/issues/1651). [noVar](https://biomejs.dev/linter/rules/no-var/) now ignores TsGlobalDeclaration. Contributed by @vasucp1207 diff --git a/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs b/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs index f594c4e0e60c..89f6ed39cfb1 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs @@ -304,7 +304,8 @@ impl Rule for UseNamingConvention { } let trimmed_name = trim_underscore_dollar(name); let actual_case = Case::identify(trimmed_name, options.strict_case); - if trimmed_name.is_empty() + if actual_case == Case::Uni + || trimmed_name.is_empty() || allowed_cases .iter() .any(|&expected_style| actual_case.is_compatible_with(expected_style)) @@ -314,7 +315,8 @@ impl Rule for UseNamingConvention { } let preferred_case = element.allowed_cases(ctx.options())[0]; let new_trimmed_name = preferred_case.convert(trimmed_name); - let suggested_name = name.replace(trimmed_name, &new_trimmed_name); + let suggested_name = (trimmed_name != new_trimmed_name) + .then(|| name.replacen(trimmed_name, &new_trimmed_name, 1)); Some(State { element, suggested_name, @@ -357,26 +359,33 @@ impl Rule for UseNamingConvention { })); } } - - Some(RuleDiagnostic::new( + let diagnostic = RuleDiagnostic::new( rule_category!(), ctx.query().syntax().text_trimmed_range(), markup! { "This "{element.to_string()}" name"{trimmed_info}" should be in "{allowed_case_names}"." }, - ).note(markup! { - "The name could be renamed to `"{suggested_name}"`." - })) + ); + Some(if let Some(suggested_name) = suggested_name { + diagnostic.note(markup! { + "The name could be renamed to `"{suggested_name}"`." + }) + } else { + diagnostic + }) } fn action(ctx: &RuleContext, state: &Self::State) -> Option { - let node = ctx.query(); - let model = ctx.model(); - let mut mutation = ctx.root().begin(); let State { element, suggested_name, } = state; + let Some(suggested_name) = suggested_name else { + return None; + }; + let node = ctx.query(); + let model = ctx.model(); + let mut mutation = ctx.root().begin(); let renamable = match node { AnyIdentifierBindingLike::JsIdentifierBinding(binding) => { if binding.is_exported(model) { @@ -452,7 +461,7 @@ impl AnyIdentifierBindingLike { #[derive(Debug)] pub(crate) struct State { element: Named, - suggested_name: String, + suggested_name: Option, } /// Rule's options. diff --git a/crates/biome_js_analyze/src/utils/case.rs b/crates/biome_js_analyze/src/utils/case.rs index b254b81669ed..1f22d14a58e4 100644 --- a/crates/biome_js_analyze/src/utils/case.rs +++ b/crates/biome_js_analyze/src/utils/case.rs @@ -22,6 +22,8 @@ pub enum Case { Pascal, /// snake_case Snake, + /// Alphanumeric Characters that cannot be in lowercase or uppercase (numbers and syllabary) + Uni, /// UPPERCASE Upper, } @@ -44,7 +46,7 @@ impl Case { /// assert_eq!(Case::identify("aHttpServer", /* no effect */ true), Case::Camel); /// assert_eq!(Case::identify("aHTTPServer", true), Case::Unknown); /// assert_eq!(Case::identify("aHTTPServer", false), Case::Camel); - /// assert_eq!(Case::identify("v8Engine", true), Case::Camel); + /// assert_eq!(Case::identify("v8Engine", /* no effect */ true), Case::Camel); /// /// assert_eq!(Case::identify("HTTP_SERVER", /* no effect */ true), Case::Constant); /// assert_eq!(Case::identify("V8_ENGINE", /* no effect */ true), Case::Constant); @@ -59,27 +61,32 @@ impl Case { /// assert_eq!(Case::identify("HttpServer", /* no effect */ true), Case::Pascal); /// assert_eq!(Case::identify("HTTPServer", true), Case::Unknown); /// assert_eq!(Case::identify("HTTPServer", false), Case::Pascal); - /// assert_eq!(Case::identify("V8Engine", true), Case::Pascal); + /// assert_eq!(Case::identify("V8Engine", /* no effect */ true), Case::Pascal); /// /// assert_eq!(Case::identify("http_server", /* no effect */ true), Case::Snake); /// /// assert_eq!(Case::identify("HTTPSERVER", /* no effect */ true), Case::Upper); /// + /// assert_eq!(Case::identify("100", /* no effect */ true), Case::Uni); + /// assert_eq!(Case::identify("안녕하세요", /* no effect */ true), Case::Uni); + /// /// assert_eq!(Case::identify("", /* no effect */ true), Case::Unknown); /// assert_eq!(Case::identify("_", /* no effect */ true), Case::Unknown); + /// assert_eq!(Case::identify("안녕하세요abc", /* no effect */ true), Case::Unknown); /// ``` pub fn identify(value: &str, strict: bool) -> Case { let mut chars = value.chars(); let Some(first_char) = chars.next() else { return Case::Unknown; }; - if !first_char.is_alphanumeric() { - return Case::Unknown; - } let mut result = if first_char.is_uppercase() { Case::NumberableCapital - } else { + } else if first_char.is_lowercase() { Case::Lower + } else if first_char.is_alphanumeric() { + Case::Uni + } else { + return Case::Unknown; }; let mut previous_char = first_char; let mut has_consecutive_uppercase = false; @@ -92,9 +99,7 @@ impl Case { '_' => match result { Case::Constant | Case::NumberableCapital | Case::Upper => Case::Constant, Case::Lower | Case::Snake => Case::Snake, - Case::Camel | Case::Kebab | Case::Pascal | Case::Unknown => { - return Case::Unknown - } + _ => return Case::Unknown, }, _ if current_char.is_uppercase() => { has_consecutive_uppercase |= previous_char.is_uppercase(); @@ -105,16 +110,20 @@ impl Case { Case::Camel | Case::Constant | Case::Pascal => result, Case::Lower => Case::Camel, Case::NumberableCapital | Case::Upper => Case::Upper, - Case::Kebab | Case::Snake | Case::Unknown => return Case::Unknown, + _ => return Case::Unknown, } } _ if current_char.is_lowercase() => match result { Case::Camel | Case::Kebab | Case::Lower | Case::Snake => result, Case::Pascal | Case::NumberableCapital => Case::Pascal, Case::Upper if !strict || !has_consecutive_uppercase => Case::Pascal, - Case::Constant | Case::Upper | Case::Unknown => return Case::Unknown, + _ => return Case::Unknown, + }, + _ if current_char.is_numeric() => result, + _ if current_char.is_alphabetic() => match result { + Case::Uni => Case::Uni, + _ => return Case::Unknown, }, - _ if current_char.is_numeric() => result, // Figures don't change the case. _ => return Case::Unknown, }; previous_char = current_char; @@ -129,6 +138,21 @@ impl Case { /// /// Any [Case] is compatible with `Case::Unknown` and with itself. /// + /// The compatibility relation between cases is depicted in the following diagram. + /// The arrow means "is compatible with". + /// + /// ```svgbob + /// ┌──► Pascal ────────────┐ + /// NumberableCapital ─┤ │ + /// └──► Upper ─► Constant ─┤ + /// ├──► Unknown + /// ┌──► Kebab ─────────────┤ + /// Lower ─┤ │ + /// └──► Camel ─────────────┤ + /// │ + /// Uni ─────────────────────────┘ + /// ``` + /// /// ### Examples /// /// ``` @@ -144,24 +168,6 @@ impl Case { /// assert!(Case::NumberableCapital.is_compatible_with(Case::Upper)); /// /// assert!(Case::Upper.is_compatible_with(Case::Constant)); - /// - /// assert!(Case::Camel.is_compatible_with(Case::Unknown)); - /// assert!(Case::Constant.is_compatible_with(Case::Unknown)); - /// assert!(Case::Kebab.is_compatible_with(Case::Unknown)); - /// assert!(Case::Lower.is_compatible_with(Case::Unknown)); - /// assert!(Case::NumberableCapital.is_compatible_with(Case::Unknown)); - /// assert!(Case::Pascal.is_compatible_with(Case::Unknown)); - /// assert!(Case::Snake.is_compatible_with(Case::Unknown)); - /// assert!(Case::Upper.is_compatible_with(Case::Unknown)); - /// - /// assert!(Case::Camel.is_compatible_with(Case::Camel)); - /// assert!(Case::Constant.is_compatible_with(Case::Constant)); - /// assert!(Case::Kebab.is_compatible_with(Case::Kebab)); - /// assert!(Case::Lower.is_compatible_with(Case::Lower)); - /// assert!(Case::NumberableCapital.is_compatible_with(Case::NumberableCapital)); - /// assert!(Case::Pascal.is_compatible_with(Case::Pascal)); - /// assert!(Case::Upper.is_compatible_with(Case::Upper)); - /// assert!(Case::Unknown.is_compatible_with(Case::Unknown)); /// ``` pub fn is_compatible_with(self, other: Case) -> bool { self == other @@ -203,32 +209,25 @@ impl Case { /// assert_eq!(Case::Snake.convert("HttpServer"), "http_server"); /// /// assert_eq!(Case::Upper.convert("Http_SERVER"), "HTTPSERVER"); - /// - /// assert_eq!(Case::Unknown.convert("_"), "_"); /// ``` pub fn convert(self, value: &str) -> String { if value.is_empty() || matches!(self, Case::Unknown) { return value.to_string(); } let mut word_separator = matches!(self, Case::Pascal); - let last_i = value.len() - 1; let mut output = String::with_capacity(value.len()); - let mut first_alphanumeric_i = 0; for ((i, current), next) in value .char_indices() .zip(value.chars().skip(1).map(Some).chain(Some(None))) { - if (i == 0 || (i == last_i)) && (current == '_' || current == '$') { - output.push(current); - first_alphanumeric_i = 1; - continue; - } - if !current.is_alphanumeric() { + if !current.is_alphanumeric() + || (matches!(self, Case::Uni) && (current.is_lowercase() || current.is_uppercase())) + { word_separator = true; continue; } if let Some(next) = next { - if i != first_alphanumeric_i && current.is_uppercase() && next.is_lowercase() { + if i != 0 && current.is_uppercase() && next.is_lowercase() { word_separator = true; } } @@ -239,6 +238,7 @@ impl Case { | Case::NumberableCapital | Case::Pascal | Case::Unknown + | Case::Uni | Case::Upper => (), Case::Constant | Case::Snake => { output.push('_'); @@ -258,11 +258,12 @@ impl Case { } Case::Constant | Case::Upper => output.extend(current.to_uppercase()), Case::NumberableCapital => { - if i == first_alphanumeric_i { + if i == 0 { output.extend(current.to_uppercase()); } } Case::Kebab | Case::Snake | Case::Lower => output.extend(current.to_lowercase()), + Case::Uni => output.extend(Some(current)), Case::Unknown => (), } word_separator = false; @@ -287,6 +288,7 @@ impl std::fmt::Display for Case { Case::NumberableCapital => "numberable capital case", Case::Pascal => "PascalCase", Case::Snake => "snake_case", + Case::Uni => "unicase", Case::Upper => "UPPERCASE", }; write!(f, "{}", repr) @@ -297,6 +299,159 @@ impl std::fmt::Display for Case { mod tests { use super::*; + #[test] + fn test_case_identify() { + let no_effect = true; + + assert_eq!(Case::identify("aHttpServer", no_effect), Case::Camel); + assert_eq!(Case::identify("aHTTPServer", true), Case::Unknown); + assert_eq!(Case::identify("aHTTPServer", false), Case::Camel); + assert_eq!(Case::identify("v8Engine", no_effect), Case::Camel); + + assert_eq!(Case::identify("HTTP_SERVER", no_effect), Case::Constant); + assert_eq!(Case::identify("V8_ENGINE", no_effect), Case::Constant); + + assert_eq!(Case::identify("http-server", no_effect), Case::Kebab); + + assert_eq!(Case::identify("httpserver", no_effect), Case::Lower); + + assert_eq!(Case::identify("T", no_effect), Case::NumberableCapital); + assert_eq!(Case::identify("T1", no_effect), Case::NumberableCapital); + + assert_eq!(Case::identify("HttpServer", no_effect), Case::Pascal); + assert_eq!(Case::identify("HTTPServer", true), Case::Unknown); + assert_eq!(Case::identify("HTTPServer", false), Case::Pascal); + assert_eq!(Case::identify("V8Engine", true), Case::Pascal); + + assert_eq!(Case::identify("http_server", no_effect), Case::Snake); + + assert_eq!(Case::identify("HTTPSERVER", no_effect), Case::Upper); + + assert_eq!(Case::identify("100", no_effect), Case::Uni); + assert_eq!(Case::identify("안녕하세요", no_effect), Case::Uni); + + assert_eq!(Case::identify("", no_effect), Case::Unknown); + assert_eq!(Case::identify("_", no_effect), Case::Unknown); + assert_eq!(Case::identify("안녕하세요ABC", no_effect), Case::Unknown); + assert_eq!(Case::identify("안녕하세요abc", no_effect), Case::Unknown); + assert_eq!(Case::identify("안녕하세요_ABC", no_effect), Case::Unknown); + assert_eq!(Case::identify("안녕하세요_abc", no_effect), Case::Unknown); + assert_eq!(Case::identify("안녕하세요-abc", no_effect), Case::Unknown); + } + + #[test] + fn test_case_is_compatible() { + assert!(Case::Unknown.is_compatible_with(Case::Unknown)); + assert!(!Case::Unknown.is_compatible_with(Case::Camel)); + assert!(!Case::Unknown.is_compatible_with(Case::Constant)); + assert!(!Case::Unknown.is_compatible_with(Case::Kebab)); + assert!(!Case::Unknown.is_compatible_with(Case::Lower)); + assert!(!Case::Unknown.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Unknown.is_compatible_with(Case::Pascal)); + assert!(!Case::Unknown.is_compatible_with(Case::Snake)); + assert!(!Case::Unknown.is_compatible_with(Case::Uni)); + assert!(!Case::Unknown.is_compatible_with(Case::Upper)); + + assert!(Case::Camel.is_compatible_with(Case::Unknown)); + assert!(Case::Camel.is_compatible_with(Case::Camel)); + assert!(!Case::Camel.is_compatible_with(Case::Constant)); + assert!(!Case::Camel.is_compatible_with(Case::Kebab)); + assert!(!Case::Camel.is_compatible_with(Case::Lower)); + assert!(!Case::Camel.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Camel.is_compatible_with(Case::Pascal)); + assert!(!Case::Camel.is_compatible_with(Case::Snake)); + assert!(!Case::Camel.is_compatible_with(Case::Uni)); + assert!(!Case::Camel.is_compatible_with(Case::Upper)); + + assert!(Case::Constant.is_compatible_with(Case::Unknown)); + assert!(!Case::Constant.is_compatible_with(Case::Camel)); + assert!(Case::Constant.is_compatible_with(Case::Constant)); + assert!(!Case::Constant.is_compatible_with(Case::Kebab)); + assert!(!Case::Constant.is_compatible_with(Case::Lower)); + assert!(!Case::Constant.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Constant.is_compatible_with(Case::Pascal)); + assert!(!Case::Constant.is_compatible_with(Case::Snake)); + assert!(!Case::Constant.is_compatible_with(Case::Uni)); + assert!(!Case::Constant.is_compatible_with(Case::Upper)); + + assert!(Case::Kebab.is_compatible_with(Case::Unknown)); + assert!(!Case::Kebab.is_compatible_with(Case::Camel)); + assert!(!Case::Kebab.is_compatible_with(Case::Constant)); + assert!(Case::Kebab.is_compatible_with(Case::Kebab)); + assert!(!Case::Kebab.is_compatible_with(Case::Lower)); + assert!(!Case::Kebab.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Kebab.is_compatible_with(Case::Pascal)); + assert!(!Case::Kebab.is_compatible_with(Case::Snake)); + assert!(!Case::Kebab.is_compatible_with(Case::Uni)); + assert!(!Case::Kebab.is_compatible_with(Case::Upper)); + + assert!(Case::Lower.is_compatible_with(Case::Unknown)); + assert!(Case::Lower.is_compatible_with(Case::Camel)); + assert!(!Case::Lower.is_compatible_with(Case::Constant)); + assert!(Case::Lower.is_compatible_with(Case::Kebab)); + assert!(Case::Lower.is_compatible_with(Case::Lower)); + assert!(!Case::Lower.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Lower.is_compatible_with(Case::Pascal)); + assert!(Case::Lower.is_compatible_with(Case::Snake)); + assert!(!Case::Lower.is_compatible_with(Case::Uni)); + assert!(!Case::Lower.is_compatible_with(Case::Upper)); + + assert!(Case::NumberableCapital.is_compatible_with(Case::Unknown)); + assert!(!Case::NumberableCapital.is_compatible_with(Case::Camel)); + assert!(Case::NumberableCapital.is_compatible_with(Case::Constant)); + assert!(!Case::NumberableCapital.is_compatible_with(Case::Kebab)); + assert!(!Case::NumberableCapital.is_compatible_with(Case::Lower)); + assert!(Case::NumberableCapital.is_compatible_with(Case::NumberableCapital)); + assert!(Case::NumberableCapital.is_compatible_with(Case::Pascal)); + assert!(!Case::NumberableCapital.is_compatible_with(Case::Snake)); + assert!(!Case::NumberableCapital.is_compatible_with(Case::Uni)); + assert!(Case::NumberableCapital.is_compatible_with(Case::Upper)); + + assert!(Case::Pascal.is_compatible_with(Case::Unknown)); + assert!(!Case::Pascal.is_compatible_with(Case::Camel)); + assert!(!Case::Pascal.is_compatible_with(Case::Constant)); + assert!(!Case::Pascal.is_compatible_with(Case::Kebab)); + assert!(!Case::Pascal.is_compatible_with(Case::Lower)); + assert!(!Case::Pascal.is_compatible_with(Case::NumberableCapital)); + assert!(Case::Pascal.is_compatible_with(Case::Pascal)); + assert!(!Case::Pascal.is_compatible_with(Case::Snake)); + assert!(!Case::Pascal.is_compatible_with(Case::Uni)); + assert!(!Case::Pascal.is_compatible_with(Case::Upper)); + + assert!(Case::Snake.is_compatible_with(Case::Unknown)); + assert!(!Case::Snake.is_compatible_with(Case::Camel)); + assert!(!Case::Snake.is_compatible_with(Case::Constant)); + assert!(!Case::Snake.is_compatible_with(Case::Kebab)); + assert!(!Case::Snake.is_compatible_with(Case::Lower)); + assert!(!Case::Snake.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Snake.is_compatible_with(Case::Pascal)); + assert!(Case::Snake.is_compatible_with(Case::Snake)); + assert!(!Case::Snake.is_compatible_with(Case::Uni)); + assert!(!Case::Snake.is_compatible_with(Case::Upper)); + + assert!(Case::Uni.is_compatible_with(Case::Unknown)); + assert!(!Case::Uni.is_compatible_with(Case::Camel)); + assert!(!Case::Uni.is_compatible_with(Case::Constant)); + assert!(!Case::Uni.is_compatible_with(Case::Kebab)); + assert!(!Case::Uni.is_compatible_with(Case::Lower)); + assert!(!Case::Uni.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Uni.is_compatible_with(Case::Pascal)); + assert!(!Case::Uni.is_compatible_with(Case::Snake)); + assert!(Case::Uni.is_compatible_with(Case::Uni)); + assert!(!Case::Uni.is_compatible_with(Case::Upper)); + + assert!(Case::Upper.is_compatible_with(Case::Unknown)); + assert!(!Case::Upper.is_compatible_with(Case::Camel)); + assert!(Case::Upper.is_compatible_with(Case::Constant)); + assert!(!Case::Upper.is_compatible_with(Case::Kebab)); + assert!(!Case::Upper.is_compatible_with(Case::Lower)); + assert!(!Case::Upper.is_compatible_with(Case::NumberableCapital)); + assert!(!Case::Upper.is_compatible_with(Case::Pascal)); + assert!(!Case::Upper.is_compatible_with(Case::Snake)); + assert!(!Case::Upper.is_compatible_with(Case::Uni)); + assert!(Case::Upper.is_compatible_with(Case::Upper)); + } + #[test] fn test_case_convert() { assert_eq!(Case::Camel.convert("camelCase"), "camelCase"); @@ -351,6 +506,9 @@ mod tests { assert_eq!(Case::Upper.convert("snake_case"), "SNAKECASE"); assert_eq!(Case::Upper.convert("Unknown_Style"), "UNKNOWNSTYLE"); + assert_eq!(Case::Uni.convert("안녕하세요"), "안녕하세요"); + assert_eq!(Case::Uni.convert("a안b녕c하_세D요E"), "안녕하세요"); + assert_eq!(Case::Unknown.convert("Unknown_Style"), "Unknown_Style"); } } diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js new file mode 100644 index 000000000000..25c32f84cafa --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js @@ -0,0 +1,11 @@ +{ + const 안녕a하세요 = 0; +} +{ + const x = { + 안녕하세요a: 0, + } +} +{ + class A안녕하세요 {} +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js.snap b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js.snap new file mode 100644 index 000000000000..fb2b3efab9ea --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidSyllabary.js.snap @@ -0,0 +1,64 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalidSyllabary.js +--- +# Input +```jsx +{ + const 안녕a하세요 = 0; +} +{ + const x = { + 안녕하세요a: 0, + } +} +{ + class A안녕하세요 {} +} +``` + +# Diagnostics +``` +invalidSyllabary.js:2:11 lint/style/useNamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This top-level const name should be in camelCase or PascalCase or CONSTANT_CASE. + + 1 │ { + > 2 │ const 안녕a하세요 = 0; + │ ^^^^^^^^^^^ + 3 │ } + 4 │ { + + +``` + +``` +invalidSyllabary.js:6:9 lint/style/useNamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This object property name should be in camelCase. + + 4 │ { + 5 │ const x = { + > 6 │ 안녕하세요a: 0, + │ ^^^^^^^^^^^ + 7 │ } + 8 │ } + + +``` + +``` +invalidSyllabary.js:10:11 lint/style/useNamingConvention ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This class name should be in PascalCase. + + 8 │ } + 9 │ { + > 10 │ class A안녕하세요 {} + │ ^^^^^^^^^^^ + 11 │ } + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js new file mode 100644 index 000000000000..29700252665d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js @@ -0,0 +1,11 @@ +{ + const 안녕하세요 = 0; +} +{ + const x = { + 안녕하세요: 0, + } +} +{ + class 안녕하세요 {} +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js.snap b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js.snap new file mode 100644 index 000000000000..d76c4d1ba598 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validSyllabary.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: validSyllabary.js +--- +# Input +```jsx +{ + const 안녕하세요 = 0; +} +{ + const x = { + 안녕하세요: 0, + } +} +{ + class 안녕하세요 {} +} +``` + + diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index 1a770bb923e6..64a0ad1f6a0a 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -205,6 +205,27 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom Contributed by @Conaclos +- [useNamingConvention](https://biomejs.dev/linter/rules/use-naming-convention) now supports [unicase](https://en.wikipedia.org/wiki/Unicase) letters ([#1786](https://github.com/biomejs/biome/issues/1786)). + + [unicase](https://en.wikipedia.org/wiki/Unicase) letters have a single case: they are neither uppercase nor lowercase. + Previously, Biome reported names in unicase as invalid. + It now accepts a name in unicase everywhere. + + The following code is now accepted: + + ```js + const 안녕하세요 = { 안녕하세요: 0 }; + ``` + + We still reject a name that mixes unicase characters with lowercase or uppercase characters: + The following names are rejected: + + ```js + const A안녕하세요 = { a안녕하세요: 0 }; + ``` + + Contributed by @Conaclos + #### Bug fixes - Fix [#1651](https://github.com/biomejs/biome/issues/1651). [noVar](https://biomejs.dev/linter/rules/no-var/) now ignores TsGlobalDeclaration. Contributed by @vasucp1207