Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement DisallowedDeclarationName rule #343

Merged
merged 49 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
15148cb
Implement DisallowedDeclarationName rule
Gyan-max Mar 8, 2025
41148c2
Fix implementation of DisallowedDeclarationName rule to properly dete…
Gyan-max Mar 9, 2025
5e3649f
Add visible marker in comments to verify push is working
Gyan-max Mar 9, 2025
87d9332
Address reviewer feedback on DisallowedDeclarationName rule
Gyan-max Mar 9, 2025
77b3457
Update DisallowedDeclarationName rule to use AST methods for type ext…
Gyan-max Mar 9, 2025
ac6bb9c
Address reviewer feedback for DisallowedDeclarationName rule
Gyan-max Mar 10, 2025
4e46190
Further improvements to DisallowedDeclarationName rule
Gyan-max Mar 10, 2025
57e3393
Update Arena.toml with gauntlet results for DisallowedDeclarationName…
Gyan-max Mar 10, 2025
23d82e4
Update DisallowedDeclarationName rule to focus on type suffixes only
Gyan-max Mar 10, 2025
b1d6a22
refactor: update DisallowedDeclarationName rule to only check suffixe…
Gyan-max Mar 10, 2025
3be5a10
fix: update DisallowedDeclarationName rule implementation and fix for…
Gyan-max Mar 11, 2025
9dc7a1d
refactor: improve DisallowedDeclarationName rule implementation and u…
Gyan-max Mar 11, 2025
8be8663
refactor: improve split_to_words function using convert_case::split
Gyan-max Mar 11, 2025
c27144f
refactor: improve match pattern in DisallowedDeclarationName rule
Gyan-max Mar 12, 2025
047a903
Merge branch 'main' into disallowed-declaration-name
Gyan-max Mar 12, 2025
a9efb11
refactor: improve match pattern in DisallowedDeclarationName rule to …
Gyan-max Mar 12, 2025
ca6c5f1
test: update expected test outputs with locally installed shellcheck
Gyan-max Mar 12, 2025
ccc0a32
Merge branch 'main' into disallowed-declaration-name
Gyan-max Mar 12, 2025
fd78b9a
Merge branch 'main' into disallowed-declaration-name
Gyan-max Mar 20, 2025
c81a990
refactor: improve disallowed_declaration_name rule
Gyan-max Mar 20, 2025
fbfd5e1
refactor: improve short type name handling in disallowed_declaration_…
Gyan-max Mar 20, 2025
5ba7346
fix: update linter tests and documentation for DisallowedDeclarationN…
Gyan-max Mar 20, 2025
ec04dea
fix: improve disallowed_declaration_name rule and tests
Gyan-max Mar 20, 2025
c9aa604
fix: improve disallowed_declaration_name rule to handle all types and…
Gyan-max Mar 20, 2025
6ceac38
fix: improve disallowed_declaration_name rule to check File and Strin…
Gyan-max Mar 20, 2025
96dc42b
fix: update disallowed_declaration_name rule to ignore File and Strin…
Gyan-max Mar 21, 2025
c626643
docs: update DisallowedDeclarationName rule explanation and fix test …
Gyan-max Mar 22, 2025
caa41d0
Merge branch 'main' into disallowed-declaration-name
Gyan-max Mar 22, 2025
bd4014d
chore: update Arena.toml diagnostics with cargo nightly
Gyan-max Mar 24, 2025
faa07d0
fix: use cmd.exe as shell on Windows for local task execution
Gyan-max Mar 24, 2025
315d5d4
Fix disallowed-declaration-name test output
Gyan-max Mar 24, 2025
5405132
Revert changes to workflow.rs
Gyan-max Mar 24, 2025
f8736f2
Fix disallowed-declaration-name test expectations
Gyan-max Mar 24, 2025
0950949
feat: add test for disallowed declaration name lint
Gyan-max Mar 24, 2025
4ed5d84
Add test for disallowed declaration name lint with blessed errors file
Gyan-max Mar 24, 2025
b892f92
Bless tests after updating source.wdl
Gyan-max Mar 24, 2025
aa43a91
Fixing source.wdl and source.errors and rebless test
Gyan-max Mar 24, 2025
e8f739f
Fix input sorting and remove misplaced directive in source.wdl and re…
Gyan-max Mar 25, 2025
15ced8a
Fix input sorting and add newline in source.wdl, rebless tests
Gyan-max Mar 25, 2025
6f13bea
Fix input sorting and directive location in source.wdl and rebless tests
Gyan-max Mar 25, 2025
ad91c91
Reorder input declarations in source.wdl to match parameter_meta and …
Gyan-max Mar 25, 2025
fe863e5
Merge branch 'main' into disallowed-declaration-name
Gyan-max Mar 25, 2025
752c1c3
chore: finishing touches
a-frantz Mar 25, 2025
da4b00d
Update RULES.md
a-frantz Mar 25, 2025
ae042c4
Update disallowed_declaration_name.rs
a-frantz Mar 25, 2025
b2fa01a
fix: remove redundant casting
a-frantz Mar 25, 2025
7fae640
chore: redundant except
a-frantz Mar 25, 2025
06a8e26
chore: review feedback
a-frantz Mar 25, 2025
b2510b0
Update CHANGELOG.md
a-frantz Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions Arena.toml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think everything ending in string and file can be ignored. Please update the logic to allow those two type suffixes and then rebless everything

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions wdl-lint/RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ be out of sync with released packages.
| `DeprecatedObject` | Deprecated | Ensures that the deprecated `Object` construct is not used. |
| `DeprecatedPlaceholderOption` | Deprecated | Ensures that the deprecated placeholder options construct is not used. |
| `DescriptionMissing` | Completeness | Ensures that each meta section has a description key. |
| `DisallowedDeclarationName` | Naming | Ensures that declaration names do not have type suffixes. |
| `DisallowedInputName` | Naming | Ensures that input names are meaningful. |
| `DisallowedOutputName` | Naming | Ensures that output names are meaningful. |
| `DoubleQuotes` | Clarity, Style | Ensures that strings are defined using double quotes. |
Expand Down
3 changes: 2 additions & 1 deletion wdl-lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ pub fn rules() -> Vec<Box<dyn Rule>> {
Box::<rules::ExpressionSpacingRule>::default(),
Box::<rules::DisallowedInputNameRule>::default(),
Box::<rules::DisallowedOutputNameRule>::default(),
Box::<rules::DisallowedDeclarationNameRule>::default(),
Box::<rules::ContainerValue>::default(),
Box::<rules::MissingRequirementsRule>::default(),
Box::<rules::UnknownRule>::default(),
Expand All @@ -134,7 +135,7 @@ pub fn rules() -> Vec<Box<dyn Rule>> {
use convert_case::Case;
use convert_case::Casing;
let mut set = std::collections::HashSet::new();
for r in rules.iter() {
for r in &rules {
if r.id().to_case(Case::Pascal) != r.id() {
panic!("lint rule id `{id}` is not pascal case", id = r.id());
}
Expand Down
2 changes: 2 additions & 0 deletions wdl-lint/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod container_value;
mod deprecated_object;
mod deprecated_placeholder_option;
mod description_missing;
mod disallowed_declaration_name;
mod disallowed_input_name;
mod disallowed_output_name;
mod double_quotes;
Expand Down Expand Up @@ -51,6 +52,7 @@ pub use container_value::*;
pub use deprecated_object::*;
pub use deprecated_placeholder_option::*;
pub use description_missing::*;
pub use disallowed_declaration_name::*;
pub use disallowed_input_name::*;
pub use disallowed_output_name::*;
pub use double_quotes::*;
Expand Down
204 changes: 204 additions & 0 deletions wdl-lint/src/rules/disallowed_declaration_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! A lint rule that disallows declaration names with type suffixes.

use std::collections::HashSet;

use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Diagnostic;
use wdl_ast::Diagnostics;
use wdl_ast::Document;
use wdl_ast::Span;
use wdl_ast::SupportedVersion;
use wdl_ast::SyntaxElement;
use wdl_ast::SyntaxKind;
use wdl_ast::VisitReason;
use wdl_ast::Visitor;
use wdl_ast::v1::BoundDecl;
use wdl_ast::v1::Decl;
use wdl_ast::v1::PrimitiveTypeKind;
use wdl_ast::v1::Type;
use wdl_ast::v1::UnboundDecl;

use crate::Rule;
use crate::Tag;
use crate::TagSet;

/// A rule that identifies declaration names that include their type names as a
/// suffix.
#[derive(Debug, Default)]
pub struct DisallowedDeclarationNameRule;

/// Create a diagnostic for a declaration identifier that contains its type name
fn decl_identifier_with_type(span: Span, decl_name: &str, type_name: &str) -> Diagnostic {
Diagnostic::warning(format!(
"declaration identifier '{decl_name}' ends with type name '{type_name}'",
))
.with_rule("DisallowedDeclarationName")
.with_highlight(span)
.with_fix("rename the identifier to not include the type name")
}

impl Rule for DisallowedDeclarationNameRule {
fn id(&self) -> &'static str {
"DisallowedDeclarationName"
}

fn description(&self) -> &'static str {
"Disallows declaration names that end with their type name"
}

fn explanation(&self) -> &'static str {
"Declaration names should not include their type as a suffix. This makes the code more \
verbose and often redundant. For example, use 'counter' instead of 'counter_int' or \
'is_active' instead of 'is_active_boolean'."
}

fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Style, Tag::Clarity])
}

fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::InputSectionNode,
SyntaxKind::OutputSectionNode,
SyntaxKind::BoundDeclNode,
SyntaxKind::UnboundDeclNode,
SyntaxKind::TaskDefinitionNode,
SyntaxKind::WorkflowDefinitionNode,
])
}
}

impl Visitor for DisallowedDeclarationNameRule {
type State = Diagnostics;

fn document(
&mut self,
_: &mut Self::State,
reason: VisitReason,
_: &Document,
_: SupportedVersion,
) {
if reason == VisitReason::Exit {
return;
}

// Reset the visitor upon document entry
*self = Default::default();
}

fn bound_decl(&mut self, state: &mut Self::State, reason: VisitReason, decl: &BoundDecl) {
if reason == VisitReason::Enter {
check_decl_name(state, &Decl::Bound(decl.clone()), &self.exceptable_nodes());
}
}

fn unbound_decl(&mut self, state: &mut Self::State, reason: VisitReason, decl: &UnboundDecl) {
if reason == VisitReason::Enter {
check_decl_name(
state,
&Decl::Unbound(decl.clone()),
&self.exceptable_nodes(),
);
}
}
}

/// Check declaration name for type suffixes
fn check_decl_name(
state: &mut Diagnostics,
decl: &Decl,
exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
let binding = decl.name();
let name = binding.text();
let ty = decl.ty();

// Extract type names to check based on the type
let mut type_names = HashSet::new();

// Handle different type variants
match ty {
Type::Ref(_) => return, // Skip type reference types (user-defined structs)
Type::Primitive(primitive_type) => {
match primitive_type.kind() {
PrimitiveTypeKind::Boolean => {
type_names.insert(primitive_type.to_string());
type_names.insert("Bool".to_string());
}
PrimitiveTypeKind::Integer => {
// Integer is shortened to Int in WDL
type_names.insert(primitive_type.to_string());
type_names.insert("Integer".to_string());
}
PrimitiveTypeKind::Float => {
type_names.insert(primitive_type.to_string());
}
PrimitiveTypeKind::Directory => {
type_names.insert(primitive_type.to_string());
type_names.insert("Dir".to_string());
}
// Include File and String types
PrimitiveTypeKind::File => {
type_names.insert(primitive_type.to_string());
}
PrimitiveTypeKind::String => {
type_names.insert(primitive_type.to_string());
}
}
}
Type::Array(_) => {
type_names.insert("Array".to_string());
}
Type::Map(_) => {
type_names.insert("Map".to_string());
}
Type::Pair(_) => {
type_names.insert("Pair".to_string());
}
Type::Object(_) => {
type_names.insert("Object".to_string());
}
}

let element = match decl {
Decl::Bound(d) => SyntaxElement::from(d.inner().clone()),
Decl::Unbound(d) => SyntaxElement::from(d.inner().clone()),
};

// Check if the declaration name ends with one of the type names
for type_name in &type_names {
let type_lower = type_name.to_lowercase();

// Special handling for short type names (3 characters or less)
// These require word-based checks to avoid false positives
if type_lower.len() <= 3 {
let words = split_to_words(name);

if let Some(last_word) = words.last() {
if last_word.to_lowercase() == type_lower {
let diagnostic = decl_identifier_with_type(binding.span(), name, type_name);
state.exceptable_add(diagnostic, element.clone(), exceptable_nodes);
return;
}
}
} else {
// For longer types, check if the identifier ends with the type name
if name.to_lowercase().ends_with(&type_lower) {
let diagnostic = decl_identifier_with_type(binding.span(), name, type_name);
state.exceptable_add(diagnostic, element.clone(), exceptable_nodes);
return;
}
}
}
}

/// Split an identifier into words using convert_case
fn split_to_words(identifier: &str) -> Vec<String> {
// Use convert_case's built-in split functionality with default boundaries
convert_case::split(&identifier, &convert_case::Boundary::defaults())
.into_iter()
.map(|s| s.to_string())
.collect()
}
2 changes: 1 addition & 1 deletion wdl-lint/tests/lints/deprecated-object/source.wdl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

version 1.1

#@ except: MissingMetas, NonmatchingOutput
#@ except: MissingMetas, NonmatchingOutput, DisallowedDeclarationName
workflow test {
#@ except: DescriptionMissing
meta {}
Expand Down
115 changes: 115 additions & 0 deletions wdl-lint/tests/lints/disallowed-declaration-name/source.errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
warning[NonmatchingOutput]: `outputs` key missing in `meta` section for the task `test_declaration_names`
┌─ tests/lints/disallowed-declaration-name/source.wdl:5:5
5 │ meta {
│ ^^^^
= fix: add an `outputs` key to `meta` section describing the outputs

note[InputSorting]: input not sorted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be here, but fixing the above should make it go away

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All done
please review it

┌─ tests/lints/disallowed-declaration-name/source.wdl:23:5
23 │ input {
│ ^^^^^ input section must be sorted
= fix: sort input statements as:
File fileInput
File gtfFile
File validName
File reference
Array[Int] arrayData
String stringValue
String genome
Boolean booleanFlag
Float floatNumber
Int my_int
Int count

warning[DisallowedDeclarationName]: declaration identifier 'gtfFile' ends with type name 'File'
┌─ tests/lints/disallowed-declaration-name/source.wdl:26:14
26 │ File gtfFile
│ ^^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'my_int' ends with type name 'Int'
┌─ tests/lints/disallowed-declaration-name/source.wdl:27:13
27 │ Int my_int
│ ^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'privateFile' ends with type name 'File'
┌─ tests/lints/disallowed-declaration-name/source.wdl:41:10
41 │ File privateFile = "sample.txt"
│ ^^^^^^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'count_int' ends with type name 'Int'
┌─ tests/lints/disallowed-declaration-name/source.wdl:42:9
42 │ Int count_int = 42
│ ^^^^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'nameString' ends with type name 'String'
┌─ tests/lints/disallowed-declaration-name/source.wdl:43:12
43 │ String nameString = "test"
│ ^^^^^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'outputFile' ends with type name 'File'
┌─ tests/lints/disallowed-declaration-name/source.wdl:49:14
49 │ File outputFile = "output.txt"
│ ^^^^^^^^^^
= fix: rename the identifier to not include the type name

note[DisallowedOutputName]: declaration identifier starts with 'output'
┌─ tests/lints/disallowed-declaration-name/source.wdl:49:14
49 │ File outputFile = "output.txt"
│ ^^^^^^^^^^
= fix: rename the identifier to not start with 'output'

warning[DisallowedDeclarationName]: declaration identifier 'result_int' ends with type name 'Int'
┌─ tests/lints/disallowed-declaration-name/source.wdl:50:13
50 │ Int result_int = 42
│ ^^^^^^^^^^
= fix: rename the identifier to not include the type name

warning[DisallowedDeclarationName]: declaration identifier 'resultString' ends with type name 'String'
┌─ tests/lints/disallowed-declaration-name/source.wdl:51:16
51 │ String resultString = "result"
│ ^^^^^^^^^^^^
= fix: rename the identifier to not include the type name

note[Whitespace]: line contains trailing whitespace
┌─ tests/lints/disallowed-declaration-name/source.wdl:55:2
55 │ }
│ ^
= fix: remove the trailing whitespace

note[EndingNewline]: missing newline at the end of the file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix this by adding a newline to source.wdl and then reblessing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also should not be here

┌─ tests/lints/disallowed-declaration-name/source.wdl:55:2
55 │ }
│ ^ expected a newline to follow this
= fix: add a newline at the end of the file

Loading
Loading