Skip to content

Commit 6cfce34

Browse files
committed
Support check preferred-local-alias
1 parent fd31961 commit 6cfce34

File tree

19 files changed

+313
-39
lines changed

19 files changed

+313
-39
lines changed

crates/emmylua_code_analysis/src/compilation/analyzer/common/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ pub fn bind_type(
1818
// type backward
1919
if type_cache.is_infer() {
2020
if let LuaTypeOwner::Decl(decl_id) = &type_owner {
21-
if let Some(refs) = db
21+
if let Some(decl_ref) = db
2222
.get_reference_index()
2323
.get_decl_references(&decl_id.file_id, decl_id)
2424
{
25-
if refs.iter().any(|it| it.is_write) {
25+
if decl_ref.mutable {
2626
match &type_cache.as_type() {
2727
LuaType::IntegerConst(_) => {
2828
type_cache = LuaTypeCache::InferType(LuaType::Integer)

crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_def_tags.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ fn get_local_stat_reference_ranges(
245245
let first_local = local_stat.child::<LuaLocalName>()?;
246246
let decl_id = LuaDeclId::new(file_id, first_local.get_position());
247247
let mut ranges = Vec::new();
248-
let refs = analyzer
248+
let decl_ref = analyzer
249249
.db
250250
.get_reference_index_mut()
251251
.get_decl_references(&file_id, &decl_id)?;
252-
for decl_ref in refs {
252+
for decl_ref in &decl_ref.cells {
253253
let syntax_id = LuaSyntaxId::new(LuaSyntaxKind::NameExpr.into(), decl_ref.range.clone());
254254
let name_node = syntax_id.to_node_from_root(&analyzer.root)?;
255255
if let Some(parent1) = name_node.parent() {

crates/emmylua_code_analysis/src/compilation/analyzer/flow/bind_analyze/stats.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,15 @@ pub fn bind_local_stat(
4444
}
4545

4646
fn check_local_immutable(binder: &mut FlowBinder, decl_id: LuaDeclId) -> bool {
47-
let Some(refs) = binder
47+
let Some(decl_ref) = binder
4848
.db
4949
.get_reference_index()
5050
.get_decl_references(&binder.file_id, &decl_id)
5151
else {
5252
return true;
5353
};
5454

55-
for r in refs {
56-
if r.is_write {
57-
return false;
58-
}
59-
}
60-
61-
true
55+
!decl_ref.mutable
6256
}
6357

6458
fn check_value_expr_is_check_expr(value_expr: LuaExpr) -> bool {

crates/emmylua_code_analysis/src/db_index/reference/file_reference.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::db_index::LuaDeclId;
55

66
#[derive(Debug)]
77
pub struct FileReference {
8-
decl_references: HashMap<LuaDeclId, Vec<DeclReference>>,
8+
decl_references: HashMap<LuaDeclId, DeclReference>,
99
references_to_decl: HashMap<TextRange, LuaDeclId>,
1010
}
1111

@@ -23,19 +23,19 @@ impl FileReference {
2323
}
2424

2525
self.references_to_decl.insert(range, decl_id);
26-
let decl_ref = DeclReference { range, is_write };
26+
let decl_ref = DeclReferenceCell { range, is_write };
2727

2828
self.decl_references
2929
.entry(decl_id)
30-
.or_insert_with(Vec::new)
31-
.push(decl_ref);
30+
.or_insert_with(DeclReference::new)
31+
.add_cell(decl_ref);
3232
}
3333

34-
pub fn get_decl_references(&self, decl_id: &LuaDeclId) -> Option<&Vec<DeclReference>> {
34+
pub fn get_decl_references(&self, decl_id: &LuaDeclId) -> Option<&DeclReference> {
3535
self.decl_references.get(decl_id)
3636
}
3737

38-
pub fn get_decl_references_map(&self) -> &HashMap<LuaDeclId, Vec<DeclReference>> {
38+
pub fn get_decl_references_map(&self) -> &HashMap<LuaDeclId, DeclReference> {
3939
&self.decl_references
4040
}
4141

@@ -45,7 +45,30 @@ impl FileReference {
4545
}
4646

4747
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
48-
pub struct DeclReference {
48+
pub struct DeclReferenceCell {
4949
pub range: TextRange,
5050
pub is_write: bool,
5151
}
52+
53+
#[derive(Debug, Clone, Eq, PartialEq)]
54+
pub struct DeclReference {
55+
pub cells: Vec<DeclReferenceCell>,
56+
pub mutable: bool,
57+
}
58+
59+
impl DeclReference {
60+
pub fn new() -> Self {
61+
Self {
62+
cells: Vec::new(),
63+
mutable: false,
64+
}
65+
}
66+
67+
pub fn add_cell(&mut self, cell: DeclReferenceCell) {
68+
if cell.is_write {
69+
self.mutable = true;
70+
}
71+
72+
self.cells.push(cell);
73+
}
74+
}

crates/emmylua_code_analysis/src/db_index/reference/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ mod string_reference;
44
use std::collections::{HashMap, HashSet};
55

66
use emmylua_parser::LuaSyntaxId;
7-
pub use file_reference::{DeclReference, FileReference};
7+
pub use file_reference::{DeclReferenceCell, FileReference};
88
use rowan::TextRange;
99
use smol_str::SmolStr;
1010
use string_reference::StringReference;
1111

12-
use crate::{FileId, InFiled};
12+
use crate::{FileId, InFiled, db_index::reference::file_reference::DeclReference};
1313

1414
use super::{LuaDeclId, LuaMemberKey, LuaTypeDeclId, traits::LuaIndex};
1515

@@ -105,7 +105,7 @@ impl LuaReferenceIndex {
105105
&self,
106106
file_id: &FileId,
107107
decl_id: &LuaDeclId,
108-
) -> Option<&Vec<DeclReference>> {
108+
) -> Option<&DeclReference> {
109109
self.file_references
110110
.get(file_id)?
111111
.get_decl_references(decl_id)
@@ -118,7 +118,7 @@ impl LuaReferenceIndex {
118118
pub fn get_decl_references_map(
119119
&self,
120120
file_id: &FileId,
121-
) -> Option<&HashMap<LuaDeclId, Vec<DeclReference>>> {
121+
) -> Option<&HashMap<LuaDeclId, DeclReference>> {
122122
self.file_references
123123
.get(file_id)
124124
.map(|file_reference| file_reference.get_decl_references_map())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod non_literal_expressions_in_assert;
2+
pub mod preferred_local_alias;
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use emmylua_parser::{
4+
LuaAst, LuaAstNode, LuaAstToken, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaLocalStat,
5+
LuaSyntaxKind,
6+
};
7+
use rowan::{NodeOrToken, TextRange};
8+
9+
use crate::{
10+
DiagnosticCode, LuaDeclId, LuaSemanticDeclId, SemanticDeclLevel, SemanticModel,
11+
diagnostic::checker::{Checker, DiagnosticContext},
12+
};
13+
14+
pub struct PreferredLocalAliasChecker;
15+
16+
impl Checker for PreferredLocalAliasChecker {
17+
const CODES: &[DiagnosticCode] = &[DiagnosticCode::PreferredLocalAlias];
18+
19+
fn check(context: &mut DiagnosticContext, semantic_model: &SemanticModel) {
20+
let mut local_alias_set = LocalAliasSet::new();
21+
let root = semantic_model.get_root().clone();
22+
for walk in root.walk_descendants::<LuaAst>() {
23+
match walk {
24+
rowan::WalkEvent::Enter(node) => {
25+
if is_scope(&node) {
26+
local_alias_set.push();
27+
}
28+
29+
match node {
30+
LuaAst::LuaLocalStat(local_stat) => {
31+
collect_local_alias(&mut local_alias_set, semantic_model, &local_stat);
32+
}
33+
LuaAst::LuaIndexExpr(index_expr) => {
34+
check_index_expr_preference(
35+
context,
36+
&local_alias_set,
37+
semantic_model,
38+
&index_expr,
39+
);
40+
}
41+
_ => {}
42+
}
43+
}
44+
rowan::WalkEvent::Leave(node) => {
45+
if is_scope(&node) {
46+
local_alias_set.pop();
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
53+
54+
fn is_scope(node: &LuaAst) -> bool {
55+
matches!(
56+
node.syntax().kind().into(),
57+
LuaSyntaxKind::Chunk | LuaSyntaxKind::Block | LuaSyntaxKind::ClosureExpr
58+
)
59+
}
60+
61+
fn collect_local_alias(
62+
local_alias_set: &mut LocalAliasSet,
63+
semantic_model: &SemanticModel,
64+
local_stat: &LuaLocalStat,
65+
) -> Option<()> {
66+
let local_list = local_stat.get_local_name_list().collect::<Vec<_>>();
67+
let value_expr = local_stat.get_value_exprs().collect::<Vec<_>>();
68+
let min_len = local_list.len().min(value_expr.len());
69+
for i in 0..min_len {
70+
let local_name = &local_list[i];
71+
let value_expr = &value_expr[i];
72+
if is_only_dot_index_expr(value_expr).unwrap_or(false) {
73+
let decl_id = LuaDeclId::new(semantic_model.get_file_id(), local_name.get_position());
74+
let decl_refs = semantic_model
75+
.get_db()
76+
.get_reference_index()
77+
.get_decl_references(&semantic_model.get_file_id(), &decl_id);
78+
if let Some(decl_refs) = decl_refs {
79+
if decl_refs.mutable {
80+
continue;
81+
}
82+
}
83+
84+
let name = match value_expr {
85+
LuaExpr::IndexExpr(index_expr) => {
86+
let index_key = index_expr.get_index_key()?;
87+
match index_key {
88+
LuaIndexKey::Name(name_token) => name_token.get_name_text().to_string(),
89+
_ => continue,
90+
}
91+
}
92+
_ => continue,
93+
};
94+
let node_or_token = NodeOrToken::Node(value_expr.syntax().clone());
95+
if let Some(semantic_id) =
96+
semantic_model.find_decl(node_or_token, SemanticDeclLevel::NoTrace)
97+
{
98+
let name_token = local_name.get_name_token()?;
99+
let preferred_name = name_token.get_name_text();
100+
101+
local_alias_set.insert(name, preferred_name.to_string(), semantic_id);
102+
local_alias_set.add_disable_check(value_expr.get_range());
103+
}
104+
}
105+
}
106+
107+
Some(())
108+
}
109+
110+
fn is_only_dot_index_expr(expr: &LuaExpr) -> Option<bool> {
111+
let mut index_expr = match expr {
112+
LuaExpr::IndexExpr(index_expr) => index_expr.clone(),
113+
_ => return Some(false),
114+
};
115+
116+
loop {
117+
let index_token = index_expr.get_index_token()?;
118+
if !index_token.is_dot() {
119+
return Some(false);
120+
}
121+
match index_expr.get_prefix_expr() {
122+
Some(LuaExpr::NameExpr(_)) => return Some(true),
123+
Some(LuaExpr::IndexExpr(prefix_index_expr)) => {
124+
index_expr = prefix_index_expr;
125+
}
126+
_ => return Some(false),
127+
}
128+
}
129+
}
130+
131+
#[derive(Debug)]
132+
struct LocalAliasSet {
133+
local_alias_stack: Vec<HashMap<String, (LuaSemanticDeclId, String)>>,
134+
disable_check: HashSet<TextRange>,
135+
}
136+
137+
impl LocalAliasSet {
138+
fn new() -> Self {
139+
LocalAliasSet {
140+
local_alias_stack: vec![HashMap::new()],
141+
disable_check: HashSet::new(),
142+
}
143+
}
144+
145+
fn push(&mut self) {
146+
self.local_alias_stack.push(HashMap::new());
147+
}
148+
149+
fn pop(&mut self) {
150+
self.local_alias_stack.pop();
151+
}
152+
153+
fn insert(&mut self, name: String, preferred_name: String, decl_id: LuaSemanticDeclId) {
154+
if let Some(map) = self.local_alias_stack.last_mut() {
155+
map.insert(name, (decl_id, preferred_name));
156+
}
157+
}
158+
159+
fn get(&self, name: &str) -> Option<(LuaSemanticDeclId, String)> {
160+
for map in self.local_alias_stack.iter().rev() {
161+
if let Some(item) = map.get(name) {
162+
return Some(item.clone());
163+
}
164+
}
165+
None
166+
}
167+
168+
fn add_disable_check(&mut self, range: TextRange) {
169+
self.disable_check.insert(range);
170+
}
171+
172+
fn is_disable_check(&self, range: &TextRange) -> bool {
173+
self.disable_check.contains(range)
174+
}
175+
}
176+
177+
fn check_index_expr_preference(
178+
context: &mut DiagnosticContext,
179+
local_alias_set: &LocalAliasSet,
180+
semantic_model: &SemanticModel,
181+
index_expr: &LuaIndexExpr,
182+
) -> Option<()> {
183+
if local_alias_set.is_disable_check(&index_expr.get_range()) {
184+
return Some(());
185+
}
186+
187+
let expr = LuaExpr::IndexExpr(index_expr.clone());
188+
if !is_only_dot_index_expr(&expr).unwrap_or(false) {
189+
return Some(());
190+
}
191+
192+
let parent = index_expr.get_parent::<LuaAst>()?;
193+
match parent {
194+
LuaAst::LuaAssignStat(assign_stat) => {
195+
let eq = assign_stat.get_assign_op()?;
196+
if eq.get_position() > index_expr.get_position() {
197+
return Some(());
198+
}
199+
}
200+
LuaAst::LuaFuncStat(_) => {
201+
return Some(());
202+
}
203+
_ => {}
204+
}
205+
206+
let index_key = index_expr.get_index_key()?;
207+
let name = match index_key {
208+
LuaIndexKey::Name(name_token) => name_token.get_name_text().to_string(),
209+
_ => return Some(()),
210+
};
211+
212+
let (semantic_id, preferred_name) = local_alias_set.get(&name)?;
213+
if semantic_model.is_reference_to(
214+
index_expr.syntax().clone(),
215+
semantic_id,
216+
SemanticDeclLevel::NoTrace,
217+
) {
218+
context.add_diagnostic(
219+
DiagnosticCode::PreferredLocalAlias,
220+
index_expr.get_range(),
221+
t!(
222+
"Prefer use local alias variable '%{name}'",
223+
name = preferred_name
224+
)
225+
.to_string(),
226+
None,
227+
);
228+
}
229+
230+
Some(())
231+
}

crates/emmylua_code_analysis/src/diagnostic/checker/local_const_reassign.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn check_local_const_reassign(
4444
let refs_index = semantic_model.get_db().get_reference_index();
4545
let local_refs = refs_index.get_local_reference(&file_id)?;
4646
let decl_refs = local_refs.get_decl_references(decl_id)?;
47-
for decl_ref in decl_refs {
47+
for decl_ref in &decl_refs.cells {
4848
if decl_ref.is_write {
4949
match attrib {
5050
LocalAttribute::Const => {

0 commit comments

Comments
 (0)