Skip to content

Commit

Permalink
feat: support type comment
Browse files Browse the repository at this point in the history
  • Loading branch information
mtshiba committed Oct 5, 2024
1 parent d3d3bdb commit 74163c4
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 9 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ pylyzer converts Python ASTs to Erg ASTs and passes them to Erg's type checker.
* [x] type narrowing (`is`, `isinstance`)
* [ ] `pyi` (stub) files support
* [ ] glob pattern file check
* [ ] `# type: ignore` directive
* [x] type comment (`# type: ...`)

## Join us!

Expand Down
88 changes: 84 additions & 4 deletions crates/py2erg/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ use erg_compiler::erg_parser::ast::{
VarRecordAttr, VarRecordAttrs, VarRecordPattern, VarSignature, VisModifierSpec,
};
use erg_compiler::erg_parser::desugar::Desugarer;
use erg_compiler::erg_parser::token::{Token, TokenKind, COLON, DOT, EQUAL};
use erg_compiler::erg_parser::token::{Token, TokenKind, AS, COLON, DOT, EQUAL};
use erg_compiler::erg_parser::Parser;
use erg_compiler::error::{CompileError, CompileErrors};
use rustpython_ast::located::LocatedMut;
use rustpython_ast::source_code::RandomLocator;
use rustpython_parser::ast::located::{
self as py_ast, Alias, Arg, Arguments, BoolOp, CmpOp, ExprConstant, Keyword, Located,
ModModule, Operator, Stmt, String, Suite, TypeParam, UnaryOp as UnOp,
};
use rustpython_parser::ast::Fold;
use rustpython_parser::source_code::{
OneIndexed, SourceLocation as PyLocation, SourceRange as PySourceRange,
};
use rustpython_parser::Parse;

use crate::ast_util::accessor_name;
use crate::error::*;
Expand Down Expand Up @@ -278,6 +282,61 @@ impl LocalContext {
}
}

#[derive(Debug, Default)]
pub struct CommentStorage {
comments: HashMap<u32, (String, Option<py_ast::Expr>)>,
}

impl fmt::Display for CommentStorage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, (comment, expr)) in &self.comments {
writeln!(f, "line {i}: \"{comment}\" (expr: {})", expr.is_some())?;
}
Ok(())
}
}

impl CommentStorage {
pub fn new() -> Self {
Self {
comments: HashMap::new(),
}
}

pub fn read(&mut self, code: &str) {
// NOTE: This locater is meaningless.
let mut locater = RandomLocator::new(code);
for (i, line) in code.lines().enumerate() {
let mut split = line.split('#');
let _code = split.next().unwrap();
if let Some(comment) = split.next() {
let comment = comment.to_string();
let trimmed = comment.trim_start();
let expr = if trimmed.starts_with("type:") {
let typ = trimmed.trim_start_matches("type:").trim();
let typ = if typ == "ignore" { "Any" } else { typ };
rustpython_ast::Expr::parse(typ, "<module>")
.ok()
.and_then(|expr| locater.fold(expr).ok())
} else {
None
};
self.comments.insert(i as u32, (comment, expr));
}
}
}

/// line: 0-origin
pub fn get_code(&self, line: u32) -> Option<&String> {
self.comments.get(&line).map(|(code, _)| code)
}

/// line: 0-origin
pub fn get_type(&self, line: u32) -> Option<&py_ast::Expr> {
self.comments.get(&line).and_then(|(_, ty)| ty.as_ref())
}
}

/// AST must be converted in the following order:
///
/// Params -> Block -> Signature
Expand Down Expand Up @@ -306,6 +365,7 @@ impl LocalContext {
pub struct ASTConverter {
cfg: ErgConfig,
shadowing: ShadowingMode,
comments: CommentStorage,
block_id_counter: usize,
block_ids: Vec<usize>,
contexts: Vec<LocalContext>,
Expand All @@ -314,10 +374,11 @@ pub struct ASTConverter {
}

impl ASTConverter {
pub fn new(cfg: ErgConfig, shadowing: ShadowingMode) -> Self {
pub fn new(cfg: ErgConfig, shadowing: ShadowingMode, comments: CommentStorage) -> Self {
Self {
shadowing,
cfg,
comments,
block_id_counter: 0,
block_ids: vec![0],
contexts: vec![LocalContext::new("<module>".into())],
Expand Down Expand Up @@ -2148,17 +2209,36 @@ impl ASTConverter {
}
let can_shadow = self.register_name_info(&name.id, NameKind::Variable);
let ident = self.convert_ident(name.id.to_string(), name.location());
let t_spec = expr
.ln_end()
.and_then(|i| {
i.checked_sub(1)
.and_then(|line| self.comments.get_type(line))
})
.cloned()
.map(|mut expr| {
// The range of `expr` is not correct, so we need to change it
if let py_ast::Expr::Subscript(sub) = &mut expr {
sub.range = name.range;
*sub.slice.range_mut() = name.range;
*sub.value.range_mut() = name.range;
} else {
*expr.range_mut() = name.range;
}
let t_as_expr = self.convert_expr(expr.clone());
TypeSpecWithOp::new(AS, self.convert_type_spec(expr), t_as_expr)
});
if can_shadow.is_yes() {
let block = Block::new(vec![expr]);
let body = DefBody::new(EQUAL, block, DefId(0));
let sig = Signature::Var(VarSignature::new(
VarPattern::Ident(ident),
None,
t_spec,
));
let def = Def::new(sig, body);
Expr::Def(def)
} else {
let redef = ReDef::new(Accessor::Ident(ident), None, expr);
let redef = ReDef::new(Accessor::Ident(ident), t_spec, expr);
Expr::ReDef(redef)
}
}
Expand Down
10 changes: 7 additions & 3 deletions crates/pylyzer_core/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use erg_compiler::error::{CompileError, CompileErrors};
use erg_compiler::module::SharedCompilerResource;
use erg_compiler::varinfo::VarInfo;
use erg_compiler::GenericHIRBuilder;
use py2erg::{dump_decl_package, ShadowingMode};
use py2erg::{dump_decl_package, CommentStorage, ShadowingMode};
use rustpython_ast::source_code::{RandomLocator, SourceRange};
use rustpython_ast::{Fold, ModModule};
use rustpython_parser::{Parse, ParseErrorType};
Expand Down Expand Up @@ -60,13 +60,15 @@ impl ASTBuildable for SimplePythonParser {
IncompleteParseArtifact<AST, ParserRunnerErrors>,
> {
let filename = self.cfg.input.filename();
let mut comments = CommentStorage::new();
comments.read(&code);
let py_program = self.parse_py_code(code)?;
let shadowing = if cfg!(feature = "debug") {
ShadowingMode::Visible
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(ErgConfig::default(), shadowing);
let converter = py2erg::ASTConverter::new(ErgConfig::default(), shadowing, comments);
let IncompleteArtifact {
object: Some(erg_module),
errors,
Expand Down Expand Up @@ -261,6 +263,8 @@ impl PythonAnalyzer {
) -> Result<CompleteArtifact, IncompleteArtifact> {
let filename = self.cfg.input.filename();
let parser = SimplePythonParser::new(self.cfg.copy());
let mut comments = CommentStorage::new();
comments.read(&py_code);
let py_program = parser
.parse_py_code(py_code)
.map_err(|iart| IncompleteArtifact::new(None, iart.errors.into(), iart.warns.into()))?;
Expand All @@ -269,7 +273,7 @@ impl PythonAnalyzer {
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(self.cfg.copy(), shadowing);
let converter = py2erg::ASTConverter::new(self.cfg.copy(), shadowing, comments);
let IncompleteArtifact {
object: Some(erg_module),
errors,
Expand Down
2 changes: 1 addition & 1 deletion tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn exec_warns() -> Result<(), String> {

#[test]
fn exec_typespec() -> Result<(), String> {
expect("tests/typespec.py", 0, 14)
expect("tests/typespec.py", 0, 15)
}

#[test]
Expand Down
6 changes: 6 additions & 0 deletions tests/typespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ def f(x: Union[int, str, None]):
f(1)
f("a")
f(None)

i1 = 1 # type: int
# ERR
i2 = 1 # type: str
i3 = 1 # type: ignore
i3 + "a" # OK

0 comments on commit 74163c4

Please sign in to comment.