diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 76645bc74d259..973140fc5040c 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1808,6 +1808,12 @@ impl RuleRunner for crate::rules::typescript::only_throw_error::OnlyThrowError { const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; } +impl RuleRunner for crate::rules::typescript::parameter_properties::ParameterProperties { + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::Class, AstType::MethodDefinition])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::typescript::prefer_as_const::PreferAsConst { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ AstType::PropertyDefinition, diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index 3460574a0be97..189629a4cdb03 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -503,6 +503,7 @@ pub use crate::rules::typescript::no_var_requires::NoVarRequires as TypescriptNo pub use crate::rules::typescript::no_wrapper_object_types::NoWrapperObjectTypes as TypescriptNoWrapperObjectTypes; pub use crate::rules::typescript::non_nullable_type_assertion_style::NonNullableTypeAssertionStyle as TypescriptNonNullableTypeAssertionStyle; pub use crate::rules::typescript::only_throw_error::OnlyThrowError as TypescriptOnlyThrowError; +pub use crate::rules::typescript::parameter_properties::ParameterProperties as TypescriptParameterProperties; pub use crate::rules::typescript::prefer_as_const::PreferAsConst as TypescriptPreferAsConst; pub use crate::rules::typescript::prefer_enum_initializers::PreferEnumInitializers as TypescriptPreferEnumInitializers; pub use crate::rules::typescript::prefer_for_of::PreferForOf as TypescriptPreferForOf; @@ -962,6 +963,7 @@ pub enum RuleEnum { TypescriptNoWrapperObjectTypes(TypescriptNoWrapperObjectTypes), TypescriptNonNullableTypeAssertionStyle(TypescriptNonNullableTypeAssertionStyle), TypescriptOnlyThrowError(TypescriptOnlyThrowError), + TypescriptParameterProperties(TypescriptParameterProperties), TypescriptPreferAsConst(TypescriptPreferAsConst), TypescriptPreferEnumInitializers(TypescriptPreferEnumInitializers), TypescriptPreferForOf(TypescriptPreferForOf), @@ -1668,7 +1670,8 @@ const TYPESCRIPT_NON_NULLABLE_TYPE_ASSERTION_STYLE_ID: usize = TYPESCRIPT_NO_WRAPPER_OBJECT_TYPES_ID + 1usize; const TYPESCRIPT_ONLY_THROW_ERROR_ID: usize = TYPESCRIPT_NON_NULLABLE_TYPE_ASSERTION_STYLE_ID + 1usize; -const TYPESCRIPT_PREFER_AS_CONST_ID: usize = TYPESCRIPT_ONLY_THROW_ERROR_ID + 1usize; +const TYPESCRIPT_PARAMETER_PROPERTIES_ID: usize = TYPESCRIPT_ONLY_THROW_ERROR_ID + 1usize; +const TYPESCRIPT_PREFER_AS_CONST_ID: usize = TYPESCRIPT_PARAMETER_PROPERTIES_ID + 1usize; const TYPESCRIPT_PREFER_ENUM_INITIALIZERS_ID: usize = TYPESCRIPT_PREFER_AS_CONST_ID + 1usize; const TYPESCRIPT_PREFER_FOR_OF_ID: usize = TYPESCRIPT_PREFER_ENUM_INITIALIZERS_ID + 1usize; const TYPESCRIPT_PREFER_FUNCTION_TYPE_ID: usize = TYPESCRIPT_PREFER_FOR_OF_ID + 1usize; @@ -2442,6 +2445,7 @@ impl RuleEnum { TYPESCRIPT_NON_NULLABLE_TYPE_ASSERTION_STYLE_ID } Self::TypescriptOnlyThrowError(_) => TYPESCRIPT_ONLY_THROW_ERROR_ID, + Self::TypescriptParameterProperties(_) => TYPESCRIPT_PARAMETER_PROPERTIES_ID, Self::TypescriptPreferAsConst(_) => TYPESCRIPT_PREFER_AS_CONST_ID, Self::TypescriptPreferEnumInitializers(_) => TYPESCRIPT_PREFER_ENUM_INITIALIZERS_ID, Self::TypescriptPreferForOf(_) => TYPESCRIPT_PREFER_FOR_OF_ID, @@ -3216,6 +3220,7 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::NAME } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::NAME, + Self::TypescriptParameterProperties(_) => TypescriptParameterProperties::NAME, Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::NAME, Self::TypescriptPreferEnumInitializers(_) => TypescriptPreferEnumInitializers::NAME, Self::TypescriptPreferForOf(_) => TypescriptPreferForOf::NAME, @@ -3992,6 +3997,7 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::CATEGORY } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::CATEGORY, + Self::TypescriptParameterProperties(_) => TypescriptParameterProperties::CATEGORY, Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::CATEGORY, Self::TypescriptPreferEnumInitializers(_) => TypescriptPreferEnumInitializers::CATEGORY, Self::TypescriptPreferForOf(_) => TypescriptPreferForOf::CATEGORY, @@ -4791,6 +4797,7 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::FIX } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::FIX, + Self::TypescriptParameterProperties(_) => TypescriptParameterProperties::FIX, Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::FIX, Self::TypescriptPreferEnumInitializers(_) => TypescriptPreferEnumInitializers::FIX, Self::TypescriptPreferForOf(_) => TypescriptPreferForOf::FIX, @@ -5610,6 +5617,9 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::documentation() } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::documentation(), + Self::TypescriptParameterProperties(_) => { + TypescriptParameterProperties::documentation() + } Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::documentation(), Self::TypescriptPreferEnumInitializers(_) => { TypescriptPreferEnumInitializers::documentation() @@ -6943,6 +6953,10 @@ impl RuleEnum { } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::config_schema(generator) .or_else(|| TypescriptOnlyThrowError::schema(generator)), + Self::TypescriptParameterProperties(_) => { + TypescriptParameterProperties::config_schema(generator) + .or_else(|| TypescriptParameterProperties::schema(generator)) + } Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::config_schema(generator) .or_else(|| TypescriptPreferAsConst::schema(generator)), Self::TypescriptPreferEnumInitializers(_) => { @@ -8375,6 +8389,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(_) => "typescript", Self::TypescriptNonNullableTypeAssertionStyle(_) => "typescript", Self::TypescriptOnlyThrowError(_) => "typescript", + Self::TypescriptParameterProperties(_) => "typescript", Self::TypescriptPreferAsConst(_) => "typescript", Self::TypescriptPreferEnumInitializers(_) => "typescript", Self::TypescriptPreferForOf(_) => "typescript", @@ -9642,6 +9657,9 @@ impl RuleEnum { Self::TypescriptOnlyThrowError(_) => Ok(Self::TypescriptOnlyThrowError( TypescriptOnlyThrowError::from_configuration(value)?, )), + Self::TypescriptParameterProperties(_) => Ok(Self::TypescriptParameterProperties( + TypescriptParameterProperties::from_configuration(value)?, + )), Self::TypescriptPreferAsConst(_) => Ok(Self::TypescriptPreferAsConst( TypescriptPreferAsConst::from_configuration(value)?, )), @@ -11216,6 +11234,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.to_configuration(), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.to_configuration(), Self::TypescriptOnlyThrowError(rule) => rule.to_configuration(), + Self::TypescriptParameterProperties(rule) => rule.to_configuration(), Self::TypescriptPreferAsConst(rule) => rule.to_configuration(), Self::TypescriptPreferEnumInitializers(rule) => rule.to_configuration(), Self::TypescriptPreferForOf(rule) => rule.to_configuration(), @@ -11896,6 +11915,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.run(node, ctx), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.run(node, ctx), Self::TypescriptOnlyThrowError(rule) => rule.run(node, ctx), + Self::TypescriptParameterProperties(rule) => rule.run(node, ctx), Self::TypescriptPreferAsConst(rule) => rule.run(node, ctx), Self::TypescriptPreferEnumInitializers(rule) => rule.run(node, ctx), Self::TypescriptPreferForOf(rule) => rule.run(node, ctx), @@ -12574,6 +12594,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.run_once(ctx), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.run_once(ctx), Self::TypescriptOnlyThrowError(rule) => rule.run_once(ctx), + Self::TypescriptParameterProperties(rule) => rule.run_once(ctx), Self::TypescriptPreferAsConst(rule) => rule.run_once(ctx), Self::TypescriptPreferEnumInitializers(rule) => rule.run_once(ctx), Self::TypescriptPreferForOf(rule) => rule.run_once(ctx), @@ -13300,6 +13321,7 @@ impl RuleEnum { rule.run_on_jest_node(jest_node, ctx) } Self::TypescriptOnlyThrowError(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::TypescriptParameterProperties(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPreferAsConst(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPreferEnumInitializers(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptPreferForOf(rule) => rule.run_on_jest_node(jest_node, ctx), @@ -14020,6 +14042,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.should_run(ctx), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.should_run(ctx), Self::TypescriptOnlyThrowError(rule) => rule.should_run(ctx), + Self::TypescriptParameterProperties(rule) => rule.should_run(ctx), Self::TypescriptPreferAsConst(rule) => rule.should_run(ctx), Self::TypescriptPreferEnumInitializers(rule) => rule.should_run(ctx), Self::TypescriptPreferForOf(rule) => rule.should_run(ctx), @@ -14796,6 +14819,9 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::IS_TSGOLINT_RULE } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::IS_TSGOLINT_RULE, + Self::TypescriptParameterProperties(_) => { + TypescriptParameterProperties::IS_TSGOLINT_RULE + } Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::IS_TSGOLINT_RULE, Self::TypescriptPreferEnumInitializers(_) => { TypescriptPreferEnumInitializers::IS_TSGOLINT_RULE @@ -15715,6 +15741,7 @@ impl RuleEnum { TypescriptNonNullableTypeAssertionStyle::HAS_CONFIG } Self::TypescriptOnlyThrowError(_) => TypescriptOnlyThrowError::HAS_CONFIG, + Self::TypescriptParameterProperties(_) => TypescriptParameterProperties::HAS_CONFIG, Self::TypescriptPreferAsConst(_) => TypescriptPreferAsConst::HAS_CONFIG, Self::TypescriptPreferEnumInitializers(_) => { TypescriptPreferEnumInitializers::HAS_CONFIG @@ -16485,6 +16512,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.types_info(), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.types_info(), Self::TypescriptOnlyThrowError(rule) => rule.types_info(), + Self::TypescriptParameterProperties(rule) => rule.types_info(), Self::TypescriptPreferAsConst(rule) => rule.types_info(), Self::TypescriptPreferEnumInitializers(rule) => rule.types_info(), Self::TypescriptPreferForOf(rule) => rule.types_info(), @@ -17163,6 +17191,7 @@ impl RuleEnum { Self::TypescriptNoWrapperObjectTypes(rule) => rule.run_info(), Self::TypescriptNonNullableTypeAssertionStyle(rule) => rule.run_info(), Self::TypescriptOnlyThrowError(rule) => rule.run_info(), + Self::TypescriptParameterProperties(rule) => rule.run_info(), Self::TypescriptPreferAsConst(rule) => rule.run_info(), Self::TypescriptPreferEnumInitializers(rule) => rule.run_info(), Self::TypescriptPreferForOf(rule) => rule.run_info(), @@ -17907,6 +17936,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( TypescriptNonNullableTypeAssertionStyle::default(), ), RuleEnum::TypescriptOnlyThrowError(TypescriptOnlyThrowError::default()), + RuleEnum::TypescriptParameterProperties(TypescriptParameterProperties::default()), RuleEnum::TypescriptPreferAsConst(TypescriptPreferAsConst::default()), RuleEnum::TypescriptPreferEnumInitializers(TypescriptPreferEnumInitializers::default()), RuleEnum::TypescriptPreferForOf(TypescriptPreferForOf::default()), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 302e2d6ef8368..7b0c99660608b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -280,6 +280,7 @@ pub(crate) mod typescript { pub mod no_wrapper_object_types; pub mod non_nullable_type_assertion_style; pub mod only_throw_error; + pub mod parameter_properties; pub mod prefer_as_const; pub mod prefer_enum_initializers; pub mod prefer_for_of; diff --git a/crates/oxc_linter/src/rules/typescript/parameter_properties.rs b/crates/oxc_linter/src/rules/typescript/parameter_properties.rs new file mode 100644 index 0000000000000..b21f6af446b4e --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/parameter_properties.rs @@ -0,0 +1,975 @@ +use rustc_hash::FxHashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use oxc_ast::{ + AstKind, + ast::{ + AssignmentExpression, AssignmentTarget, Class, ClassElement, Expression, FormalParameter, + MethodDefinition, MethodDefinitionKind, PropertyDefinition, PropertyKey, Statement, + TSAccessibility, + }, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Atom, GetSpan, Span}; + +use crate::{ + AstNode, + context::{ContextHost, LintContext}, + rule::{DefaultRuleConfig, Rule}, +}; + +fn prefer_class_property_diagnostic(parameter: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Property {parameter} should be declared as a class property.")) + .with_help("Remove the parameter modifier and declare this member on the class instead.") + .with_label(span) +} + +fn prefer_parameter_property_diagnostic(parameter: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("Property {parameter} should be declared as a parameter property.")) + .with_help("Declare this member as a constructor parameter property and remove the class field plus assignment.") + .with_label(span) +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "kebab-case")] +enum Prefer { + #[default] + ClassProperty, + ParameterProperty, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +enum Modifier { + #[serde(rename = "private")] + Private, + #[serde(rename = "private readonly")] + PrivateReadonly, + #[serde(rename = "protected")] + Protected, + #[serde(rename = "protected readonly")] + ProtectedReadonly, + #[serde(rename = "public")] + Public, + #[serde(rename = "public readonly")] + PublicReadonly, + #[serde(rename = "readonly")] + Readonly, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase", default, deny_unknown_fields)] +struct ParameterPropertiesConfig { + /// Modifiers that are allowed to be used with parameter properties or class properties, depending on the `prefer` option. + allow: Vec, + /// Whether to prefer parameter properties or class properties. + prefer: Prefer, +} + +#[derive(Debug, Default, Clone, Deserialize, JsonSchema)] +pub struct ParameterProperties(Box); + +declare_oxc_lint!( + /// ### What it does + /// + /// Requires or disallows parameter properties in class constructors. + /// + /// ### Why is this bad? + /// + /// Mixing parameter properties and class property declarations can make + /// class style inconsistent and harder to maintain. + /// + /// ### Examples + /// + /// #### `{ "prefer": "class-property" }` (default) + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// class Foo { + /// constructor(private name: string) {} + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// class Foo { + /// name: string; + /// constructor(name: string) { + /// this.name = name; + /// } + /// } + /// ``` + /// + /// #### `{ "prefer": "parameter-property" }` + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// class Foo { + /// name: string; + /// constructor(name: string) { + /// this.name = name; + /// } + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// class Foo { + /// constructor(private name: string) {} + /// } + /// ``` + ParameterProperties, + typescript, + style, + config = ParameterPropertiesConfig, +); + +impl Rule for ParameterProperties { + fn from_configuration(value: serde_json::Value) -> Result { + serde_json::from_value::>(value).map(DefaultRuleConfig::into_inner) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::MethodDefinition(method) + if method.kind == MethodDefinitionKind::Constructor + && self.0.prefer == Prefer::ClassProperty => + { + self.check_prefer_class_property(method, ctx); + } + AstKind::Class(class) if self.0.prefer == Prefer::ParameterProperty => { + self.check_prefer_parameter_property(class, ctx); + } + _ => {} + } + } + + fn should_run(&self, ctx: &ContextHost) -> bool { + ctx.source_type().is_typescript() + } +} + +#[derive(Default)] +struct PropertyNodes<'a> { + class_property: Option<&'a PropertyDefinition<'a>>, + constructor_assignment: Option<&'a AssignmentExpression<'a>>, + constructor_parameter: Option<&'a FormalParameter<'a>>, +} + +impl ParameterProperties { + fn check_prefer_class_property<'a>( + &self, + method: &MethodDefinition<'a>, + ctx: &LintContext<'a>, + ) { + for parameter in &method.value.params.items { + if !parameter.has_modifier() { + continue; + } + + if self.is_allowed_modifier(modifier_from_parameter(parameter)) { + continue; + } + + let Some(name) = parameter.pattern.get_binding_identifier().map(|id| id.name) else { + continue; + }; + + ctx.diagnostic(prefer_class_property_diagnostic(name.as_str(), parameter.span)); + } + } + + fn check_prefer_parameter_property<'a>(&self, class: &Class<'a>, ctx: &LintContext<'a>) { + let mut property_nodes_by_name = FxHashMap::, PropertyNodes<'a>>::default(); + + for element in &class.body.body { + let ClassElement::PropertyDefinition(property) = element else { continue }; + if property.computed || property.value.is_some() { + continue; + } + let PropertyKey::StaticIdentifier(identifier) = &property.key else { continue }; + if self.is_allowed_modifier(modifier_from_property(property)) { + continue; + } + + property_nodes_by_name.entry(identifier.name.into()).or_default().class_property = + Some(property); + } + + for element in &class.body.body { + let ClassElement::MethodDefinition(method) = element else { continue }; + if method.kind != MethodDefinitionKind::Constructor { + continue; + } + + for parameter in &method.value.params.items { + if parameter.initializer.is_some() { + continue; + } + let Some(identifier) = parameter.pattern.get_binding_identifier() else { continue }; + property_nodes_by_name + .entry(identifier.name.into()) + .or_default() + .constructor_parameter = Some(parameter); + } + + for statement in + method.value.body.as_ref().map_or([].as_slice(), |body| &body.statements) + { + let Some((assignment, name)) = constructor_assignment(statement) else { break }; + property_nodes_by_name.entry(name).or_default().constructor_assignment = + Some(assignment); + } + } + + for (name, nodes) in property_nodes_by_name { + let (Some(class_property), Some(_assignment), Some(constructor_parameter)) = + (nodes.class_property, nodes.constructor_assignment, nodes.constructor_parameter) + else { + continue; + }; + + if !type_annotations_match(class_property, constructor_parameter, ctx) { + continue; + } + + ctx.diagnostic(prefer_parameter_property_diagnostic( + name.as_str(), + class_property.span, + )); + } + } + + fn is_allowed_modifier(&self, modifier: Option) -> bool { + modifier.is_some_and(|modifier| self.0.allow.contains(&modifier)) + } +} + +fn modifier_from_accessibility( + accessibility: Option, + readonly: bool, +) -> Option { + match (accessibility, readonly) { + (Some(TSAccessibility::Private), true) => Some(Modifier::PrivateReadonly), + (Some(TSAccessibility::Private), false) => Some(Modifier::Private), + (Some(TSAccessibility::Protected), true) => Some(Modifier::ProtectedReadonly), + (Some(TSAccessibility::Protected), false) => Some(Modifier::Protected), + (Some(TSAccessibility::Public), true) => Some(Modifier::PublicReadonly), + (Some(TSAccessibility::Public), false) => Some(Modifier::Public), + (None, true) => Some(Modifier::Readonly), + (None, false) => None, + } +} + +fn modifier_from_parameter(parameter: &FormalParameter<'_>) -> Option { + modifier_from_accessibility(parameter.accessibility, parameter.readonly) +} + +fn modifier_from_property(property: &PropertyDefinition<'_>) -> Option { + modifier_from_accessibility(property.accessibility, property.readonly) +} + +fn constructor_assignment<'a>( + statement: &'a Statement<'a>, +) -> Option<(&'a AssignmentExpression<'a>, Atom<'a>)> { + let Statement::ExpressionStatement(expression_statement) = statement else { + return None; + }; + let Expression::AssignmentExpression(assignment) = &expression_statement.expression else { + return None; + }; + let AssignmentTarget::StaticMemberExpression(member_expression) = &assignment.left else { + return None; + }; + if !matches!(member_expression.object.get_inner_expression(), Expression::ThisExpression(_)) { + return None; + } + let Expression::Identifier(identifier) = assignment.right.get_inner_expression() else { + return None; + }; + if member_expression.property.name != identifier.name { + return None; + } + Some((assignment, identifier.name.into())) +} + +fn type_annotations_match( + class_property: &PropertyDefinition<'_>, + constructor_parameter: &FormalParameter<'_>, + ctx: &LintContext<'_>, +) -> bool { + match (&class_property.type_annotation, &constructor_parameter.type_annotation) { + (None, None) => true, + (Some(left), Some(right)) => { + ctx.source_range(left.span()) == ctx.source_range(right.span()) + } + _ => false, + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + class Foo { + constructor(name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string) {} + } + ", + Some(serde_json::json!([{ "prefer": "class-property" }])), + ), + ( + " + class Foo { + constructor(...name: string[]) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string, age: number) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string) {} + constructor(name: string, age?: number) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["readonly"] }])), + ), + ( + " + class Foo { + constructor(private name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["private"] }])), + ), + ( + " + class Foo { + constructor(protected name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["protected"] }])), + ), + ( + " + class Foo { + constructor(public name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["public"] }])), + ), + ( + " + class Foo { + constructor(private readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["private readonly"] }])), + ), + ( + " + class Foo { + constructor(protected readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["protected readonly"] }])), + ), + ( + " + class Foo { + constructor(public readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["public readonly"] }])), + ), + ( + " + class Foo { + constructor( + readonly name: string, + private age: number, + ) {} + } + ", + Some(serde_json::json!([{ "allow": ["readonly", "private"] }])), + ), + ( + " + class Foo { + constructor( + public readonly name: string, + private age: number, + ) {} + } + ", + Some(serde_json::json!([{ "allow": ["public readonly", "private"] }])), + ), + ( + " + class Foo { + constructor(private name: string[]) {} + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + constructor(...name: string[]) {} + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + constructor(age: string, ...name: string[]) {} + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + constructor( + private age: string, + ...name: string[] + ) {} + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age: number; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age = ''; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age: string; + constructor(age) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age: string; + constructor(age: string) { + console.log('unrelated'); + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + other: string; + constructor(age: string) { + this.other = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + prop: string; + other: string; + constructor(prop: string) { + this.other = prop; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + age: string; + constructor(age: string) { + this.age = ''; + console.log(age); + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + age() { + return ''; + } + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "allow": ["public"], "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public readonly age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some( + serde_json::json!([{ "allow": ["public readonly"], "prefer": "parameter-property" }]), + ), + ), + ( + " + class Foo { + protected age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "allow": ["protected"], "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + protected readonly age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some( + serde_json::json!([ { "allow": ["protected readonly"], "prefer": "parameter-property" }, ]), + ), + ), + ( + " + class Foo { + private age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some(serde_json::json!([{ "allow": ["private"], "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + private readonly age: string; + constructor(age: string) { + this.age = age; + } + } + ", + Some( + serde_json::json!([{ "allow": ["private readonly"], "prefer": "parameter-property" }]), + ), + ), + ]; + + let fail = vec![ + ( + " + class Foo { + constructor(readonly name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(private name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(protected name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(public name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(private readonly name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(protected readonly name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(public readonly name: string) {} + } + ", + None, + ), + ( + " + class Foo { + constructor( + public name: string, + age: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor( + private name: string, + private age: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor( + protected name: string, + protected age: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor( + public name: string, + public age: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string) {} + constructor( + private name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(private name: string) {} + constructor( + private name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(private name: string) {} + constructor( + private name: string, + private age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string) {} + constructor( + protected name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(protected name: string) {} + constructor( + protected name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(protected name: string) {} + constructor( + protected name: string, + protected age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(name: string) {} + constructor( + public name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(public name: string) {} + constructor( + public name: string, + age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(public name: string) {} + constructor( + public name: string, + public age?: number, + ) {} + } + ", + None, + ), + ( + " + class Foo { + constructor(readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["private"] }])), + ), + ( + " + class Foo { + constructor(private name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["readonly"] }])), + ), + ( + " + class Foo { + constructor(protected name: string) {} + } + ", + Some( + serde_json::json!([ { "allow": ["readonly", "private", "public", "protected readonly"], }, ]), + ), + ), + ( + " + class Foo { + constructor(public name: string) {} + } + ", + Some( + serde_json::json!([ { "allow": [ "readonly", "private", "protected", "protected readonly", "public readonly", ], }, ]), + ), + ), + ( + " + class Foo { + constructor(private readonly name: string) {} + } + ", + Some(serde_json::json!([{ "allow": ["readonly", "private"] }])), + ), + ( + " + class Foo { + constructor(protected readonly name: string) {} + } + ", + Some( + serde_json::json!([ { "allow": [ "readonly", "protected", "private readonly", "public readonly", ], }, ]), + ), + ), + ( + " + class Foo { + constructor(private name: string) {} + constructor( + private name: string, + protected age?: number, + ) {} + } + ", + Some(serde_json::json!([{ "allow": ["private"] }])), + ), + ( + " + class Foo { + member: string; + + constructor(member: string) { + this.member = member; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + constructor(member: string) { + this.member = member; + } + + member: string; + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + member; + constructor(member) { + this.member = member; + } + } + ", + Some(serde_json::json!([{ "prefer": "parameter-property" }])), + ), + ( + " + class Foo { + public member: string; + constructor(member: string) { + this.member = member; + } + } + ", + Some( + serde_json::json!([ { "allow": ["protected", "private", "readonly"], "prefer": "parameter-property", }, ]), + ), + ), + ]; + + Tester::new(ParameterProperties::NAME, ParameterProperties::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/typescript_parameter_properties.snap b/crates/oxc_linter/src/snapshots/typescript_parameter_properties.snap new file mode 100644 index 0000000000000..31bfeabcd99e2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/typescript_parameter_properties.snap @@ -0,0 +1,390 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(readonly name: string) {} + · ───────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private name: string) {} + · ──────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected name: string) {} + · ────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(public name: string) {} + · ─────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private readonly name: string) {} + · ───────────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected readonly name: string) {} + · ─────────────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(public readonly name: string) {} + · ──────────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:4:17] + 3 │ constructor( + 4 │ public name: string, + · ─────────────────── + 5 │ age: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:4:17] + 3 │ constructor( + 4 │ private name: string, + · ──────────────────── + 5 │ private age: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ private name: string, + 5 │ private age: number, + · ─────────────────── + 6 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:4:17] + 3 │ constructor( + 4 │ protected name: string, + · ────────────────────── + 5 │ protected age: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ protected name: string, + 5 │ protected age: number, + · ───────────────────── + 6 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:4:17] + 3 │ constructor( + 4 │ public name: string, + · ─────────────────── + 5 │ public age: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ public name: string, + 5 │ public age: number, + · ────────────────── + 6 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ private name: string, + · ──────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private name: string) {} + · ──────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ private name: string, + · ──────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private name: string) {} + · ──────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ private name: string, + · ──────────────────── + 6 │ private age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:6:17] + 5 │ private name: string, + 6 │ private age?: number, + · ──────────────────── + 7 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ protected name: string, + · ────────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected name: string) {} + · ────────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ protected name: string, + · ────────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected name: string) {} + · ────────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ protected name: string, + · ────────────────────── + 6 │ protected age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:6:17] + 5 │ protected name: string, + 6 │ protected age?: number, + · ────────────────────── + 7 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ public name: string, + · ─────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(public name: string) {} + · ─────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ public name: string, + · ─────────────────── + 6 │ age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(public name: string) {} + · ─────────────────── + 4 │ constructor( + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:5:17] + 4 │ constructor( + 5 │ public name: string, + · ─────────────────── + 6 │ public age?: number, + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:6:17] + 5 │ public name: string, + 6 │ public age?: number, + · ─────────────────── + 7 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(readonly name: string) {} + · ───────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private name: string) {} + · ──────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected name: string) {} + · ────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(public name: string) {} + · ─────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(private readonly name: string) {} + · ───────────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property name should be declared as a class property. + ╭─[parameter_properties.tsx:3:27] + 2 │ class Foo { + 3 │ constructor(protected readonly name: string) {} + · ─────────────────────────────── + 4 │ } + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property age should be declared as a class property. + ╭─[parameter_properties.tsx:6:17] + 5 │ private name: string, + 6 │ protected age?: number, + · ────────────────────── + 7 │ ) {} + ╰──── + help: Remove the parameter modifier and declare this member on the class instead. + + ⚠ typescript-eslint(parameter-properties): Property member should be declared as a parameter property. + ╭─[parameter_properties.tsx:3:15] + 2 │ class Foo { + 3 │ member: string; + · ─────────────── + 4 │ + ╰──── + help: Declare this member as a constructor parameter property and remove the class field plus assignment. + + ⚠ typescript-eslint(parameter-properties): Property member should be declared as a parameter property. + ╭─[parameter_properties.tsx:7:15] + 6 │ + 7 │ member: string; + · ─────────────── + 8 │ } + ╰──── + help: Declare this member as a constructor parameter property and remove the class field plus assignment. + + ⚠ typescript-eslint(parameter-properties): Property member should be declared as a parameter property. + ╭─[parameter_properties.tsx:3:15] + 2 │ class Foo { + 3 │ member; + · ─────── + 4 │ constructor(member) { + ╰──── + help: Declare this member as a constructor parameter property and remove the class field plus assignment. + + ⚠ typescript-eslint(parameter-properties): Property member should be declared as a parameter property. + ╭─[parameter_properties.tsx:3:15] + 2 │ class Foo { + 3 │ public member: string; + · ────────────────────── + 4 │ constructor(member: string) { + ╰──── + help: Declare this member as a constructor parameter property and remove the class field plus assignment.