typegen: add bare minimum parsing of qml files
This commit is contained in:
		
							parent
							
								
									7590fd3246
								
							
						
					
					
						commit
						34cfae3023
					
				
					 3 changed files with 170 additions and 14 deletions
				
			
		| 
						 | 
					@ -35,18 +35,44 @@ fn main() -> anyhow::Result<()> {
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				.collect::<Result<HashMap<_, _>, _>>()?;
 | 
									.collect::<Result<HashMap<_, _>, _>>()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let parser = parse::Parser::new();
 | 
								let qml_texts = module
 | 
				
			||||||
 | 
									.header
 | 
				
			||||||
 | 
									.qml_files
 | 
				
			||||||
 | 
									.iter()
 | 
				
			||||||
 | 
									.map(|file| {
 | 
				
			||||||
 | 
										let text = std::fs::read_to_string(dir.join(file)).with_context(|| {
 | 
				
			||||||
 | 
											format!(
 | 
				
			||||||
 | 
												"failed to read module qml file `{file}` at {:?}",
 | 
				
			||||||
 | 
												dir.join(file)
 | 
				
			||||||
 | 
											)
 | 
				
			||||||
 | 
										})?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										Ok::<_, anyhow::Error>((file, text))
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.collect::<Result<HashMap<_, _>, _>>()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let header_parser = parse::CppParser::new();
 | 
				
			||||||
 | 
								let qml_parser = parse::QmlParser::new();
 | 
				
			||||||
			let mut ctx = parse::ParseContext::new(&module.header.name);
 | 
								let mut ctx = parse::ParseContext::new(&module.header.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			texts
 | 
								texts
 | 
				
			||||||
				.iter()
 | 
									.iter()
 | 
				
			||||||
				.map(|(header, text)| {
 | 
									.map(|(header, text)| {
 | 
				
			||||||
					parser
 | 
										header_parser
 | 
				
			||||||
						.parse(&text, &mut ctx)
 | 
											.parse(&text, &mut ctx)
 | 
				
			||||||
						.with_context(|| format!("while parsing module header `{header}`"))
 | 
											.with_context(|| format!("while parsing module header `{header}`"))
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				.collect::<Result<_, _>>()?;
 | 
									.collect::<Result<_, _>>()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								qml_texts
 | 
				
			||||||
 | 
									.iter()
 | 
				
			||||||
 | 
									.map(|(file, text)| {
 | 
				
			||||||
 | 
										qml_parser
 | 
				
			||||||
 | 
											.parse(&file, &text, &mut ctx)
 | 
				
			||||||
 | 
											.with_context(|| format!("while parsing module qml file `{file}`"))
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.collect::<Result<_, _>>()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let typespec = ctx.gen_typespec(&module.header.name);
 | 
								let typespec = ctx.gen_typespec(&module.header.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let text = serde_json::to_string_pretty(&typespec).unwrap();
 | 
								let text = serde_json::to_string_pretty(&typespec).unwrap();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
use std::borrow::Cow;
 | 
					use std::{borrow::Cow, collections::HashMap};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use anyhow::{anyhow, bail, Context};
 | 
					use anyhow::{anyhow, bail, Context};
 | 
				
			||||||
use fancy_regex::Regex;
 | 
					use fancy_regex::Regex;
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,10 @@ use crate::typespec;
 | 
				
			||||||
pub struct ModuleInfoHeader {
 | 
					pub struct ModuleInfoHeader {
 | 
				
			||||||
	pub name: String,
 | 
						pub name: String,
 | 
				
			||||||
	pub description: String,
 | 
						pub description: String,
 | 
				
			||||||
 | 
						#[serde(default)]
 | 
				
			||||||
	pub headers: Vec<String>,
 | 
						pub headers: Vec<String>,
 | 
				
			||||||
 | 
						#[serde(default)]
 | 
				
			||||||
 | 
						pub qml_files: Vec<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
| 
						 | 
					@ -50,7 +53,7 @@ pub struct ClassInfo<'a> {
 | 
				
			||||||
	pub type_: ClassType,
 | 
						pub type_: ClassType,
 | 
				
			||||||
	pub name: &'a str,
 | 
						pub name: &'a str,
 | 
				
			||||||
	pub qml_name: Option<&'a str>,
 | 
						pub qml_name: Option<&'a str>,
 | 
				
			||||||
	pub superclass: Option<&'a str>,
 | 
						pub superclass: Option<Cow<'a, str>>,
 | 
				
			||||||
	pub singleton: bool,
 | 
						pub singleton: bool,
 | 
				
			||||||
	pub uncreatable: bool,
 | 
						pub uncreatable: bool,
 | 
				
			||||||
	pub comment: Option<Comment<'a>>,
 | 
						pub comment: Option<Comment<'a>>,
 | 
				
			||||||
| 
						 | 
					@ -66,9 +69,9 @@ pub enum ClassType {
 | 
				
			||||||
	Gadget,
 | 
						Gadget,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone, Copy)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
pub struct Property<'a> {
 | 
					pub struct Property<'a> {
 | 
				
			||||||
	pub type_: &'a str,
 | 
						pub type_: Cow<'a, str>,
 | 
				
			||||||
	pub name: &'a str,
 | 
						pub name: &'a str,
 | 
				
			||||||
	pub comment: Option<Comment<'a>>,
 | 
						pub comment: Option<Comment<'a>>,
 | 
				
			||||||
	pub readable: bool,
 | 
						pub readable: bool,
 | 
				
			||||||
| 
						 | 
					@ -112,7 +115,7 @@ pub struct Variant<'a> {
 | 
				
			||||||
	pub comment: Option<Comment<'a>>,
 | 
						pub comment: Option<Comment<'a>>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Parser {
 | 
					pub struct CppParser {
 | 
				
			||||||
	pub class_regex: Regex,
 | 
						pub class_regex: Regex,
 | 
				
			||||||
	pub macro_regex: Regex,
 | 
						pub macro_regex: Regex,
 | 
				
			||||||
	pub property_regex: Regex,
 | 
						pub property_regex: Regex,
 | 
				
			||||||
| 
						 | 
					@ -126,6 +129,14 @@ pub struct Parser {
 | 
				
			||||||
	pub enum_variant_regex: Regex,
 | 
						pub enum_variant_regex: Regex,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct QmlParser {
 | 
				
			||||||
 | 
						pub class_regex: Regex,
 | 
				
			||||||
 | 
						pub alias_regex: Regex,
 | 
				
			||||||
 | 
						pub safe_body_regex: Regex,
 | 
				
			||||||
 | 
						pub property_regex: Regex,
 | 
				
			||||||
 | 
						pub function_regex: Regex,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub struct ParseContext<'a> {
 | 
					pub struct ParseContext<'a> {
 | 
				
			||||||
	pub module: &'a str,
 | 
						pub module: &'a str,
 | 
				
			||||||
| 
						 | 
					@ -143,7 +154,7 @@ impl<'a> ParseContext<'a> {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Parser {
 | 
					impl CppParser {
 | 
				
			||||||
	pub fn new() -> Self {
 | 
						pub fn new() -> Self {
 | 
				
			||||||
		Self {
 | 
							Self {
 | 
				
			||||||
			class_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*class\s+(?<name>\w+)(?:\s*:\s*public\s+((?<super>\w+)(<.+>)?)(\s*,(\s*\w+)*)*)?\s*\{(?<body>[\s\S]*?)(?!};\s*Q_ENUM)};"#).unwrap(),
 | 
								class_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*class\s+(?<name>\w+)(?:\s*:\s*public\s+((?<super>\w+)(<.+>)?)(\s*,(\s*\w+)*)*)?\s*\{(?<body>[\s\S]*?)(?!};\s*Q_ENUM)};"#).unwrap(),
 | 
				
			||||||
| 
						 | 
					@ -235,7 +246,7 @@ impl Parser {
 | 
				
			||||||
								}
 | 
													}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
								properties.push(Property {
 | 
													properties.push(Property {
 | 
				
			||||||
									type_: prop.name("type").unwrap().as_str(),
 | 
														type_: Cow::Borrowed(prop.name("type").unwrap().as_str()),
 | 
				
			||||||
									name: prop.name("name").unwrap().as_str(),
 | 
														name: prop.name("name").unwrap().as_str(),
 | 
				
			||||||
									comment: comment.map(|v| Comment::new(v, ctx.module)),
 | 
														comment: comment.map(|v| Comment::new(v, ctx.module)),
 | 
				
			||||||
									readable: read || member,
 | 
														readable: read || member,
 | 
				
			||||||
| 
						 | 
					@ -364,7 +375,7 @@ impl Parser {
 | 
				
			||||||
				type_,
 | 
									type_,
 | 
				
			||||||
				name,
 | 
									name,
 | 
				
			||||||
				qml_name,
 | 
									qml_name,
 | 
				
			||||||
				superclass,
 | 
									superclass: superclass.map(|s| Cow::Borrowed(s)),
 | 
				
			||||||
				singleton,
 | 
									singleton,
 | 
				
			||||||
				uncreatable,
 | 
									uncreatable,
 | 
				
			||||||
				comment: comment.map(|v| Comment::new(v, ctx.module)),
 | 
									comment: comment.map(|v| Comment::new(v, ctx.module)),
 | 
				
			||||||
| 
						 | 
					@ -461,6 +472,102 @@ impl Parser {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// todo: use an actual parser (never)
 | 
				
			||||||
 | 
					impl QmlParser {
 | 
				
			||||||
 | 
						pub fn new() -> Self {
 | 
				
			||||||
 | 
							Self {
 | 
				
			||||||
 | 
								class_regex: Regex::new(r#"((?<aliases>(\s*\/\/\/ alias.+)+)[\s\S]*?)?(?<comment>(\s*\/\/\/.*\n)+)?\s*(?<super>[A-Z]\w*)\s+{(?<body>[\s\S]*)}"#).unwrap(),
 | 
				
			||||||
 | 
								alias_regex: Regex::new(r#"alias\s+(?<alias>\w+)\s+(?<definition>[\w.]+)"#).unwrap(),
 | 
				
			||||||
 | 
								safe_body_regex: Regex::new(r#"((?<safebody1>^[\s\S]*?)(\n\s+[A-Z]\w* {[\s\S]*)|(?<safebody2>^[\s\S]*))"#).unwrap(),
 | 
				
			||||||
 | 
								// note: can pick up function bodies
 | 
				
			||||||
 | 
								property_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*(?<default>default\s+)?(?<required>required\s+)?(?<readonly>readonly\s+)?property\s+(\/\*(?<typeoverride>\w+)\*\/)?(?<type>\w+)\s+(?<name>\w+)\s*(:\s*(?<definition>(.*{\n[\s\S]*?\/\/ END-DEF|.*?(?!{)\n)))?"#).unwrap(),
 | 
				
			||||||
 | 
								// note: can pick up prop bodies
 | 
				
			||||||
 | 
								function_regex: Regex::new(r#"(?<comment>(\s*\/\/\/.*\n)+)?\s*function\s+(?<name>\w+)\s*\((?<params>.*)\)\s*:\s*(?<return>\w+)\s*{"#).unwrap(),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pub fn parse<'a>(
 | 
				
			||||||
 | 
							&self,
 | 
				
			||||||
 | 
							filename: &'a str,
 | 
				
			||||||
 | 
							text: &'a str,
 | 
				
			||||||
 | 
							ctx: &mut ParseContext<'a>,
 | 
				
			||||||
 | 
						) -> anyhow::Result<()> {
 | 
				
			||||||
 | 
							for class in self.class_regex.captures_iter(text) {
 | 
				
			||||||
 | 
								let class = class?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let mut aliases = HashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if let Some(aliases_str) = class.name("aliases").map(|m| m.as_str()) {
 | 
				
			||||||
 | 
									for alias in self.alias_regex.captures_iter(aliases_str) {
 | 
				
			||||||
 | 
										let alias = alias?;
 | 
				
			||||||
 | 
										aliases.insert(
 | 
				
			||||||
 | 
											alias.name("alias").unwrap().as_str(),
 | 
				
			||||||
 | 
											alias.name("definition").unwrap().as_str(),
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let alias_lookup = |name: &'a str| *aliases.get(name).unwrap_or(&name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let comment = class.name("comment").map(|m| m.as_str());
 | 
				
			||||||
 | 
								let superclass = class.name("super").unwrap().as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let body = {
 | 
				
			||||||
 | 
									let body = class.name("body").unwrap().as_str();
 | 
				
			||||||
 | 
									let sbc = self
 | 
				
			||||||
 | 
										.safe_body_regex
 | 
				
			||||||
 | 
										.captures(body)?
 | 
				
			||||||
 | 
										.ok_or_else(|| anyhow!("unable to capture safebody"))?;
 | 
				
			||||||
 | 
									sbc.name("safebody1")
 | 
				
			||||||
 | 
										.or_else(|| sbc.name("safebody2"))
 | 
				
			||||||
 | 
										.unwrap()
 | 
				
			||||||
 | 
										.as_str()
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let mut properties = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for prop in self.property_regex.captures_iter(body) {
 | 
				
			||||||
 | 
									let prop = prop?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									let type_ = prop
 | 
				
			||||||
 | 
										.name("typeoverride")
 | 
				
			||||||
 | 
										.or_else(|| prop.name("type"))
 | 
				
			||||||
 | 
										.unwrap()
 | 
				
			||||||
 | 
										.as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									properties.push(Property {
 | 
				
			||||||
 | 
										type_: Cow::Owned(format!("QML:{}", alias_lookup(type_))),
 | 
				
			||||||
 | 
										name: prop.name("name").unwrap().as_str(),
 | 
				
			||||||
 | 
										comment: prop
 | 
				
			||||||
 | 
											.name("comment")
 | 
				
			||||||
 | 
											.map(|m| Comment::new(m.as_str(), ctx.module)),
 | 
				
			||||||
 | 
										readable: true,
 | 
				
			||||||
 | 
										writable: prop.name("readonly").is_none(),
 | 
				
			||||||
 | 
										default: prop.name("default").is_some(),
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let name = filename.split_once('.').unwrap().0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ctx.classes.push(ClassInfo {
 | 
				
			||||||
 | 
									type_: ClassType::Object,
 | 
				
			||||||
 | 
									name,
 | 
				
			||||||
 | 
									qml_name: Some(name),
 | 
				
			||||||
 | 
									superclass: Some(Cow::Owned(format!("QML:{}", superclass))),
 | 
				
			||||||
 | 
									singleton: false,
 | 
				
			||||||
 | 
									uncreatable: false,
 | 
				
			||||||
 | 
									comment: comment.map(|v| Comment::new(v, ctx.module)),
 | 
				
			||||||
 | 
									properties,
 | 
				
			||||||
 | 
									invokables: Vec::new(),
 | 
				
			||||||
 | 
									signals: Vec::new(),
 | 
				
			||||||
 | 
									enums: Vec::new(),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Ok(())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ParseContext<'_> {
 | 
					impl ParseContext<'_> {
 | 
				
			||||||
	pub fn gen_typespec(&self, module: &str) -> typespec::TypeSpec {
 | 
						pub fn gen_typespec(&self, module: &str) -> typespec::TypeSpec {
 | 
				
			||||||
		typespec::TypeSpec {
 | 
							typespec::TypeSpec {
 | 
				
			||||||
| 
						 | 
					@ -506,10 +613,10 @@ impl ParseContext<'_> {
 | 
				
			||||||
						description,
 | 
											description,
 | 
				
			||||||
						details,
 | 
											details,
 | 
				
			||||||
						// filters gadgets
 | 
											// filters gadgets
 | 
				
			||||||
						superclass: class.superclass?.to_string(),
 | 
											superclass: class.superclass.clone()?.to_string(),
 | 
				
			||||||
						singleton: class.singleton,
 | 
											singleton: class.singleton,
 | 
				
			||||||
						uncreatable: class.uncreatable,
 | 
											uncreatable: class.uncreatable,
 | 
				
			||||||
						properties: class.properties.iter().map(|p| (*p).into()).collect(),
 | 
											properties: class.properties.iter().map(|p| p.clone().into()).collect(),
 | 
				
			||||||
						functions: class.invokables.iter().map(|f| f.as_typespec()).collect(),
 | 
											functions: class.invokables.iter().map(|f| f.as_typespec()).collect(),
 | 
				
			||||||
						signals: class.signals.iter().map(|s| s.as_typespec()).collect(),
 | 
											signals: class.signals.iter().map(|s| s.as_typespec()).collect(),
 | 
				
			||||||
						enums: class
 | 
											enums: class
 | 
				
			||||||
| 
						 | 
					@ -543,7 +650,7 @@ impl ParseContext<'_> {
 | 
				
			||||||
				.filter_map(|class| match class.type_ {
 | 
									.filter_map(|class| match class.type_ {
 | 
				
			||||||
					ClassType::Gadget => Some(typespec::Gadget {
 | 
										ClassType::Gadget => Some(typespec::Gadget {
 | 
				
			||||||
						cname: class.name.to_string(),
 | 
											cname: class.name.to_string(),
 | 
				
			||||||
						properties: class.properties.iter().map(|p| (*p).into()).collect(),
 | 
											properties: class.properties.iter().map(|p| p.clone().into()).collect(),
 | 
				
			||||||
					}),
 | 
										}),
 | 
				
			||||||
					_ => None,
 | 
										_ => None,
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,17 @@ pub fn resolve_types(
 | 
				
			||||||
		.iter()
 | 
							.iter()
 | 
				
			||||||
		.filter(|type_| type_.module.as_ref().map(|v| v as &str) == Some(module));
 | 
							.filter(|type_| type_.module.as_ref().map(|v| v as &str) == Some(module));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let findqmltype = |cname: &str| typespec.typemap.iter().find(|type_| type_.cname == cname);
 | 
						let findqmltype = |name: &str| {
 | 
				
			||||||
 | 
							if name.starts_with("QML:") {
 | 
				
			||||||
 | 
								println!("QML? {name}");
 | 
				
			||||||
 | 
								typespec
 | 
				
			||||||
 | 
									.typemap
 | 
				
			||||||
 | 
									.iter()
 | 
				
			||||||
 | 
									.find(|type_| type_.name == name[4..])
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								typespec.typemap.iter().find(|type_| type_.cname == name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for mapping in types {
 | 
						for mapping in types {
 | 
				
			||||||
		let Some(class) = typespec
 | 
							let Some(class) = typespec
 | 
				
			||||||
| 
						 | 
					@ -57,6 +67,19 @@ pub fn resolve_types(
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fn qmlparamtype(ctype: &str, typespec: &TypeSpec) -> outform::Type {
 | 
							fn qmlparamtype(ctype: &str, typespec: &TypeSpec) -> outform::Type {
 | 
				
			||||||
 | 
								if ctype.starts_with("QML:") {
 | 
				
			||||||
 | 
									return match typespec
 | 
				
			||||||
 | 
										.typemap
 | 
				
			||||||
 | 
										.iter()
 | 
				
			||||||
 | 
										.find(|type_| type_.name == ctype[4..])
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										Some(t) => {
 | 
				
			||||||
 | 
											outform::Type::resolve(t.module.as_ref().map(|v| v as &str), &t.name)
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										None => outform::Type::unknown(),
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let (mut ctype, of) = match ctype.split_once('<') {
 | 
								let (mut ctype, of) = match ctype.split_once('<') {
 | 
				
			||||||
				None => (ctype, None),
 | 
									None => (ctype, None),
 | 
				
			||||||
				Some((ctype, mut remaining)) => {
 | 
									Some((ctype, mut remaining)) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue