-
Notifications
You must be signed in to change notification settings - Fork 28
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
Changes from 25 commits
15148cb
41148c2
5e3649f
87d9332
77b3457
ac6bb9c
4e46190
57e3393
23d82e4
b1d6a22
3be5a10
9dc7a1d
8be8663
c27144f
047a903
a9efb11
ca6c5f1
ccc0a32
fd78b9a
c81a990
fbfd5e1
5ba7346
ec04dea
c9aa604
6ceac38
96dc42b
c626643
caa41d0
bd4014d
faa07d0
315d5d4
5405132
f8736f2
0950949
4ed5d84
b892f92
aa43a91
e8f739f
15ced8a
6f13bea
ad91c91
fe863e5
752c1c3
da4b00d
ae042c4
b2fa01a
7fae640
06a8e26
b2510b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Gyan-max marked this conversation as resolved.
Show resolved
Hide resolved
Gyan-max marked this conversation as resolved.
Show resolved
Hide resolved
|
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 { | ||
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"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, | ||
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(); | ||
peterhuene marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Handle different type variants | ||
match ty { | ||
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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()); | ||
Gyan-max marked this conversation as resolved.
Show resolved
Hide resolved
|
||
type_names.insert("Integer".to_string()); | ||
} | ||
PrimitiveTypeKind::Float => { | ||
type_names.insert(primitive_type.to_string()); | ||
} | ||
PrimitiveTypeKind::Directory => { | ||
type_names.insert(primitive_type.to_string()); | ||
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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> { | ||
Gyan-max marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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() | ||
} |
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
a-frantz marked this conversation as resolved.
Show resolved
Hide resolved
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All done |
||
┌─ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fix this by adding a newline to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
Gyan-max marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
There was a problem hiding this comment.
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
andfile
can be ignored. Please update the logic to allow those two type suffixes and then rebless everything