mirror of
https://github.com/nushell/nushell.git
synced 2024-11-15 13:04:34 +01:00
Add stmt parsing
This commit is contained in:
parent
3d2e227f11
commit
e3abadd686
14
src/main.rs
14
src/main.rs
@ -2,19 +2,11 @@ use engine_q::{lex, lite_parse, LexMode, ParserWorkingSet};
|
|||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
if let Some(path) = std::env::args().nth(1) {
|
if let Some(path) = std::env::args().nth(1) {
|
||||||
let file = std::fs::read(&path)?;
|
|
||||||
|
|
||||||
// let (output, err) = lex(&file, 0, 0, LexMode::Normal);
|
|
||||||
|
|
||||||
// println!("{:?} tokens, error: {:?}", output, err);
|
|
||||||
|
|
||||||
// let (output, err) = lite_parse(&output);
|
|
||||||
|
|
||||||
// println!("{:?}, error: {:?}", output, err);
|
|
||||||
|
|
||||||
let mut working_set = ParserWorkingSet::new(None);
|
let mut working_set = ParserWorkingSet::new(None);
|
||||||
|
|
||||||
let (output, err) = working_set.parse_file(&path, &file);
|
//let file = std::fs::read(&path)?;
|
||||||
|
//let (output, err) = working_set.parse_file(&path, &file);
|
||||||
|
let (output, err) = working_set.parse_source(path.as_bytes());
|
||||||
println!("{:?} {:?}", output, err);
|
println!("{:?} {:?}", output, err);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -4,4 +4,6 @@ pub use crate::Span;
|
|||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
ExtraTokens(Span),
|
ExtraTokens(Span),
|
||||||
UnexpectedEof(String, Span),
|
UnexpectedEof(String, Span),
|
||||||
|
UnknownStatement(Span),
|
||||||
|
Mismatch(String, Span),
|
||||||
}
|
}
|
||||||
|
320
src/parser.rs
320
src/parser.rs
@ -1,9 +1,71 @@
|
|||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
|
|
||||||
use crate::{lex, lite_parse, LiteBlock, LiteStatement, ParseError, ParserWorkingSet, Span};
|
use crate::{
|
||||||
|
lex, lite_parse,
|
||||||
|
parser_state::{Type, VarId},
|
||||||
|
LiteBlock, LiteCommand, LiteStatement, ParseError, ParserWorkingSet, Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The syntactic shapes that values must match to be passed into a command. You can think of this as the type-checking that occurs when you call a function.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum SyntaxShape {
|
||||||
|
/// Any syntactic form is allowed
|
||||||
|
Any,
|
||||||
|
/// Strings and string-like bare words are allowed
|
||||||
|
String,
|
||||||
|
/// A dotted path to navigate the table
|
||||||
|
ColumnPath,
|
||||||
|
/// A dotted path to navigate the table (including variable)
|
||||||
|
FullColumnPath,
|
||||||
|
/// Only a numeric (integer or decimal) value is allowed
|
||||||
|
Number,
|
||||||
|
/// A range is allowed (eg, `1..3`)
|
||||||
|
Range,
|
||||||
|
/// Only an integer value is allowed
|
||||||
|
Int,
|
||||||
|
/// A filepath is allowed
|
||||||
|
FilePath,
|
||||||
|
/// A glob pattern is allowed, eg `foo*`
|
||||||
|
GlobPattern,
|
||||||
|
/// A block is allowed, eg `{start this thing}`
|
||||||
|
Block,
|
||||||
|
/// A table is allowed, eg `[first second]`
|
||||||
|
Table,
|
||||||
|
/// A filesize value is allowed, eg `10kb`
|
||||||
|
Filesize,
|
||||||
|
/// A duration value is allowed, eg `19day`
|
||||||
|
Duration,
|
||||||
|
/// An operator
|
||||||
|
Operator,
|
||||||
|
/// A math expression which expands shorthand forms on the lefthand side, eg `foo > 1`
|
||||||
|
/// The shorthand allows us to more easily reach columns inside of the row being passed in
|
||||||
|
RowCondition,
|
||||||
|
/// A general math expression, eg the `1 + 2` of `= 1 + 2`
|
||||||
|
MathExpression,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Expression {}
|
pub enum Expr {
|
||||||
|
Int(i64),
|
||||||
|
Var(VarId),
|
||||||
|
Garbage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Expression {
|
||||||
|
expr: Expr,
|
||||||
|
ty: Type,
|
||||||
|
span: Span,
|
||||||
|
}
|
||||||
|
impl Expression {
|
||||||
|
pub fn garbage(span: Span) -> Expression {
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Garbage,
|
||||||
|
span,
|
||||||
|
ty: Type::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Import {}
|
pub enum Import {}
|
||||||
@ -13,6 +75,12 @@ pub struct Block {
|
|||||||
stmts: Vec<Statement>,
|
stmts: Vec<Statement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Block {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { stmts: vec![] }
|
Self { stmts: vec![] }
|
||||||
@ -21,8 +89,8 @@ impl Block {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VarDecl {
|
pub struct VarDecl {
|
||||||
name: String,
|
var_id: VarId,
|
||||||
value: Expression,
|
expression: Expression,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -30,19 +98,64 @@ pub enum Statement {
|
|||||||
Pipeline(Pipeline),
|
Pipeline(Pipeline),
|
||||||
VarDecl(VarDecl),
|
VarDecl(VarDecl),
|
||||||
Import(Import),
|
Import(Import),
|
||||||
|
Expression(Expression),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Pipeline {}
|
pub struct Pipeline {}
|
||||||
|
|
||||||
|
impl Default for Pipeline {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Pipeline {
|
impl Pipeline {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {}
|
Self {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn garbage(span: Span) -> Expression {
|
||||||
|
Expression::garbage(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(spans: &[Span]) -> Span {
|
||||||
|
let length = spans.len();
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
Span::unknown()
|
||||||
|
} else if length == 1 || spans[0].file_id != spans[length - 1].file_id {
|
||||||
|
spans[0]
|
||||||
|
} else {
|
||||||
|
Span {
|
||||||
|
start: spans[0].start,
|
||||||
|
end: spans[length - 1].end,
|
||||||
|
file_id: spans[0].file_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ParserWorkingSet {
|
impl ParserWorkingSet {
|
||||||
|
/*
|
||||||
|
fn parse_let(&mut self, command: &LiteCommand) -> (Statement, Option<ParseError>) {
|
||||||
|
|
||||||
|
}
|
||||||
|
fn parse_special_command(&mut self, command: &LiteCommand) -> (Statement, Option<ParseError>) {
|
||||||
|
let command_name = self.get_span_contents(command.parts[0]);
|
||||||
|
println!("{:?}", command_name);
|
||||||
|
match command_name {
|
||||||
|
b"let" => self.parse_let(command),
|
||||||
|
b"def" => self.parse_def(command),
|
||||||
|
b"source" => self.parse_source(command),
|
||||||
|
_ => (
|
||||||
|
Statement::None,
|
||||||
|
Some(ParseError::UnknownStatement(command.parts[0])),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_statement(
|
fn parse_statement(
|
||||||
&mut self,
|
&mut self,
|
||||||
block: &mut Block,
|
block: &mut Block,
|
||||||
@ -50,20 +163,182 @@ impl ParserWorkingSet {
|
|||||||
) -> Option<ParseError> {
|
) -> Option<ParseError> {
|
||||||
match lite_pipeline.commands.len() {
|
match lite_pipeline.commands.len() {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => {
|
1 => None,
|
||||||
let command_name = self.get_span_contents(lite_pipeline.commands[0].parts[0]);
|
|
||||||
println!("{:?}", command_name);
|
|
||||||
if command_name == b"let" {
|
|
||||||
println!("found let")
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
// pipeline
|
// pipeline
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub fn parse_int(&mut self, token: &str, span: Span) -> (Expression, Option<ParseError>) {
|
||||||
|
if let Some(token) = token.strip_prefix("0x") {
|
||||||
|
if let Ok(v) = i64::from_str_radix(token, 16) {
|
||||||
|
(
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Int(v),
|
||||||
|
ty: Type::Int,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("int".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if let Some(token) = token.strip_prefix("0b") {
|
||||||
|
if let Ok(v) = i64::from_str_radix(token, 2) {
|
||||||
|
(
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Int(v),
|
||||||
|
ty: Type::Int,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("int".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if let Some(token) = token.strip_prefix("0o") {
|
||||||
|
if let Ok(v) = i64::from_str_radix(token, 8) {
|
||||||
|
(
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Int(v),
|
||||||
|
ty: Type::Int,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("int".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if let Ok(x) = token.parse::<i64>() {
|
||||||
|
(
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Int(x),
|
||||||
|
ty: Type::Int,
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("int".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_number(&mut self, token: &str, span: Span) -> (Expression, Option<ParseError>) {
|
||||||
|
if let (x, None) = self.parse_int(token, span) {
|
||||||
|
(x, None)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("number".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_arg(
|
||||||
|
&mut self,
|
||||||
|
span: Span,
|
||||||
|
shape: SyntaxShape,
|
||||||
|
) -> (Expression, Option<ParseError>) {
|
||||||
|
match shape {
|
||||||
|
SyntaxShape::Number => {
|
||||||
|
if let Ok(token) = String::from_utf8(self.get_span_contents(span).into()) {
|
||||||
|
self.parse_number(&token, span)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("number".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
garbage(span),
|
||||||
|
Some(ParseError::Mismatch("number".into(), span)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_math_expression(&mut self, spans: &[Span]) -> (Expression, Option<ParseError>) {
|
||||||
|
self.parse_arg(spans[0], SyntaxShape::Number)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_expression(&mut self, spans: &[Span]) -> (Expression, Option<ParseError>) {
|
||||||
|
self.parse_math_expression(spans)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_variable(&mut self, span: Span) -> Option<ParseError> {
|
||||||
|
let contents = self.get_span_contents(span);
|
||||||
|
|
||||||
|
if !contents.is_empty() && contents[0] == b'$' {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ParseError::Mismatch("variable".into(), span))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_keyword(&self, span: Span, keyword: &[u8]) -> Option<ParseError> {
|
||||||
|
if self.get_span_contents(span) == keyword {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ParseError::Mismatch(
|
||||||
|
String::from_utf8_lossy(keyword).to_string(),
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_let(&mut self, spans: &[Span]) -> (Statement, Option<ParseError>) {
|
||||||
|
let mut error = None;
|
||||||
|
if spans.len() >= 4 && self.parse_keyword(spans[0], b"let").is_none() {
|
||||||
|
let err = self.parse_variable(spans[1]);
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
let err = self.parse_keyword(spans[2], b"=");
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
let (expression, err) = self.parse_expression(&spans[3..]);
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
let var_name: Vec<_> = self.get_span_contents(spans[1]).into();
|
||||||
|
let var_id = self.add_variable(var_name, expression.ty);
|
||||||
|
|
||||||
|
(Statement::VarDecl(VarDecl { var_id, expression }), error)
|
||||||
|
} else {
|
||||||
|
let span = span(spans);
|
||||||
|
(
|
||||||
|
Statement::Expression(garbage(span)),
|
||||||
|
Some(ParseError::Mismatch("let".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_statement(&mut self, spans: &[Span]) -> (Statement, Option<ParseError>) {
|
||||||
|
if let (stmt, None) = self.parse_let(spans) {
|
||||||
|
(stmt, None)
|
||||||
|
} else if let (expr, None) = self.parse_expression(spans) {
|
||||||
|
(Statement::Expression(expr), None)
|
||||||
|
} else {
|
||||||
|
let span = span(spans);
|
||||||
|
(
|
||||||
|
Statement::Expression(garbage(span)),
|
||||||
|
Some(ParseError::Mismatch("statement".into(), span)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_block(&mut self, lite_block: &LiteBlock) -> (Block, Option<ParseError>) {
|
pub fn parse_block(&mut self, lite_block: &LiteBlock) -> (Block, Option<ParseError>) {
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
@ -72,8 +347,10 @@ impl ParserWorkingSet {
|
|||||||
let mut block = Block::new();
|
let mut block = Block::new();
|
||||||
|
|
||||||
for pipeline in &lite_block.block {
|
for pipeline in &lite_block.block {
|
||||||
let err = self.parse_statement(&mut block, pipeline);
|
let (stmt, err) = self.parse_statement(&pipeline.commands[0].parts);
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
|
|
||||||
|
block.stmts.push(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.exit_scope();
|
self.exit_scope();
|
||||||
@ -99,4 +376,21 @@ impl ParserWorkingSet {
|
|||||||
|
|
||||||
(output, error)
|
(output, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_source(&mut self, source: &[u8]) -> (Block, Option<ParseError>) {
|
||||||
|
let mut error = None;
|
||||||
|
|
||||||
|
let file_id = self.add_file("source".into(), source.into());
|
||||||
|
|
||||||
|
let (output, err) = lex(source, file_id, 0, crate::LexMode::Normal);
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
let (output, err) = lite_parse(&output);
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
let (output, err) = self.parse_block(&output);
|
||||||
|
error = error.or(err);
|
||||||
|
|
||||||
|
(output, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::Span;
|
use crate::{ParseError, Span};
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
pub struct ParserState {
|
pub struct ParserState {
|
||||||
@ -10,11 +10,16 @@ pub enum VarLocation {
|
|||||||
OuterScope,
|
OuterScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum Type {}
|
pub enum Type {
|
||||||
|
Int,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type VarId = usize;
|
||||||
|
|
||||||
struct ScopeFrame {
|
struct ScopeFrame {
|
||||||
vars: HashMap<String, Type>,
|
vars: HashMap<Vec<u8>, VarId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopeFrame {
|
impl ScopeFrame {
|
||||||
@ -27,6 +32,7 @@ impl ScopeFrame {
|
|||||||
|
|
||||||
pub struct ParserWorkingSet {
|
pub struct ParserWorkingSet {
|
||||||
files: Vec<(String, Vec<u8>)>,
|
files: Vec<(String, Vec<u8>)>,
|
||||||
|
vars: HashMap<VarId, Type>,
|
||||||
permanent_state: Option<Arc<ParserState>>,
|
permanent_state: Option<Arc<ParserState>>,
|
||||||
scope: Vec<ScopeFrame>,
|
scope: Vec<ScopeFrame>,
|
||||||
}
|
}
|
||||||
@ -73,6 +79,7 @@ impl ParserWorkingSet {
|
|||||||
pub fn new(permanent_state: Option<Arc<ParserState>>) -> Self {
|
pub fn new(permanent_state: Option<Arc<ParserState>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
files: vec![],
|
files: vec![],
|
||||||
|
vars: HashMap::new(),
|
||||||
permanent_state,
|
permanent_state,
|
||||||
scope: vec![],
|
scope: vec![],
|
||||||
}
|
}
|
||||||
@ -115,23 +122,38 @@ impl ParserWorkingSet {
|
|||||||
self.scope.push(ScopeFrame::new());
|
self.scope.push(ScopeFrame::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_variable(&self, name: &str) -> Option<(VarLocation, Type)> {
|
pub fn find_variable(&self, name: &[u8]) -> Option<(VarLocation, Type)> {
|
||||||
for scope in self.scope.iter().rev().enumerate() {
|
for scope in self.scope.iter().rev().enumerate() {
|
||||||
if let Some(result) = scope.1.vars.get(name) {
|
if let Some(result) = scope.1.vars.get(name) {
|
||||||
if scope.0 == 0 {
|
if let Some(result) = self.vars.get(result) {
|
||||||
// Top level
|
if scope.0 == 0 {
|
||||||
return Some((VarLocation::CurrentScope, result.clone()));
|
// Top level
|
||||||
} else {
|
return Some((VarLocation::CurrentScope, result.clone()));
|
||||||
return Some((VarLocation::OuterScope, result.clone()));
|
} else {
|
||||||
|
return Some((VarLocation::OuterScope, result.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {}
|
pub fn add_variable(&mut self, name: Vec<u8>, ty: Type) -> VarId {
|
||||||
|
let last = self
|
||||||
|
.scope
|
||||||
|
.last_mut()
|
||||||
|
.expect("internal error: missing stack frame");
|
||||||
|
|
||||||
|
let next_id = self.vars.len();
|
||||||
|
|
||||||
|
last.vars.insert(name, next_id);
|
||||||
|
|
||||||
|
self.vars.insert(next_id, ty);
|
||||||
|
|
||||||
|
next_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod parser_state_tests {
|
mod parser_state_tests {
|
||||||
|
@ -13,4 +13,12 @@ impl Span {
|
|||||||
file_id,
|
file_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unknown() -> Span {
|
||||||
|
Span {
|
||||||
|
start: usize::MAX,
|
||||||
|
end: usize::MAX,
|
||||||
|
file_id: usize::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user