|
| 1 | +use std::fmt; |
| 2 | + |
| 3 | +/// Represents a distinct entry in the database schema. |
| 4 | +pub enum Entry { |
| 5 | + /// An entry defining a database table. |
| 6 | + Table(Table), |
| 7 | + |
| 8 | + /// An entry defining type that is a union of other types. |
| 9 | + Union(Union), |
| 10 | +} |
| 11 | + |
| 12 | +/// A table in the database schema. |
| 13 | +pub struct Table { |
| 14 | + pub name: String, |
| 15 | + pub columns: Vec<Column>, |
| 16 | + pub keysets: Option<Vec<String>>, |
| 17 | +} |
| 18 | + |
| 19 | +/// A union in the database schema. |
| 20 | +pub struct Union { |
| 21 | + pub name: String, |
| 22 | + pub members: Vec<String>, |
| 23 | +} |
| 24 | + |
| 25 | +/// A column in a table. |
| 26 | +pub struct Column { |
| 27 | + pub db_type: DbColumnType, |
| 28 | + pub name: String, |
| 29 | + pub unique: bool, |
| 30 | + pub ql_type: QlColumnType, |
| 31 | + pub ql_type_is_ref: bool, |
| 32 | +} |
| 33 | + |
| 34 | +/// The database column type. |
| 35 | +pub enum DbColumnType { |
| 36 | + Int, |
| 37 | + String, |
| 38 | +} |
| 39 | + |
| 40 | +// The QL type of a column. |
| 41 | +pub enum QlColumnType { |
| 42 | + /// Primitive `int` type. |
| 43 | + Int, |
| 44 | + |
| 45 | + /// Primitive `string` type. |
| 46 | + String, |
| 47 | + |
| 48 | + /// A custom type, defined elsewhere by a table or union. |
| 49 | + Custom(String), |
| 50 | +} |
| 51 | + |
| 52 | +const RESERVED_KEYWORDS: [&'static str; 14] = [ |
| 53 | + "boolean", "case", "date", "float", "int", "key", "of", "order", "ref", "string", "subtype", |
| 54 | + "type", "unique", "varchar", |
| 55 | +]; |
| 56 | + |
| 57 | +/// Returns a string that's a copy of `name` but suitably escaped to be a valid |
| 58 | +/// QL identifier. |
| 59 | +pub fn escape_name(name: &str) -> String { |
| 60 | + let mut result = String::new(); |
| 61 | + |
| 62 | + // If there's a leading underscore, replace it with 'underscore_'. |
| 63 | + if let Some(c) = name.chars().next() { |
| 64 | + if c == '_' { |
| 65 | + result.push_str("underscore"); |
| 66 | + } |
| 67 | + } |
| 68 | + for c in name.chars() { |
| 69 | + match c { |
| 70 | + '{' => result.push_str("lbrace"), |
| 71 | + '}' => result.push_str("rbrace"), |
| 72 | + '<' => result.push_str("langle"), |
| 73 | + '>' => result.push_str("rangle"), |
| 74 | + '[' => result.push_str("lbracket"), |
| 75 | + ']' => result.push_str("rbracket"), |
| 76 | + '(' => result.push_str("lparen"), |
| 77 | + ')' => result.push_str("rparen"), |
| 78 | + '|' => result.push_str("pipe"), |
| 79 | + '=' => result.push_str("equal"), |
| 80 | + '~' => result.push_str("tilde"), |
| 81 | + '?' => result.push_str("question"), |
| 82 | + '`' => result.push_str("backtick"), |
| 83 | + '^' => result.push_str("caret"), |
| 84 | + '!' => result.push_str("bang"), |
| 85 | + '#' => result.push_str("hash"), |
| 86 | + '%' => result.push_str("percent"), |
| 87 | + '&' => result.push_str("ampersand"), |
| 88 | + '.' => result.push_str("dot"), |
| 89 | + ',' => result.push_str("comma"), |
| 90 | + '/' => result.push_str("slash"), |
| 91 | + ':' => result.push_str("colon"), |
| 92 | + ';' => result.push_str("semicolon"), |
| 93 | + '"' => result.push_str("dquote"), |
| 94 | + '*' => result.push_str("star"), |
| 95 | + '+' => result.push_str("plus"), |
| 96 | + '-' => result.push_str("minus"), |
| 97 | + '@' => result.push_str("at"), |
| 98 | + _ => result.push_str(&c.to_lowercase().to_string()), |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + for &keyword in &RESERVED_KEYWORDS { |
| 103 | + if result == keyword { |
| 104 | + result.push_str("__"); |
| 105 | + break; |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + result |
| 110 | +} |
| 111 | + |
| 112 | +impl fmt::Display for Table { |
| 113 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 114 | + if let Some(keyset) = &self.keysets { |
| 115 | + write!(f, "#keyset[")?; |
| 116 | + for (key_index, key) in keyset.iter().enumerate() { |
| 117 | + if key_index > 0 { |
| 118 | + write!(f, ", ")?; |
| 119 | + } |
| 120 | + write!(f, "{}", key)?; |
| 121 | + } |
| 122 | + write!(f, "]\n")?; |
| 123 | + } |
| 124 | + |
| 125 | + write!(f, "{}(\n", self.name)?; |
| 126 | + for (column_index, column) in self.columns.iter().enumerate() { |
| 127 | + write!(f, " ")?; |
| 128 | + if column.unique { |
| 129 | + write!(f, "unique ")?; |
| 130 | + } |
| 131 | + write!( |
| 132 | + f, |
| 133 | + "{} ", |
| 134 | + match column.db_type { |
| 135 | + DbColumnType::Int => "int", |
| 136 | + DbColumnType::String => "string", |
| 137 | + } |
| 138 | + )?; |
| 139 | + write!(f, "{}: ", column.name)?; |
| 140 | + match &column.ql_type { |
| 141 | + QlColumnType::Int => write!(f, "int")?, |
| 142 | + QlColumnType::String => write!(f, "string")?, |
| 143 | + QlColumnType::Custom(name) => write!(f, "@{}", name)?, |
| 144 | + } |
| 145 | + if column.ql_type_is_ref { |
| 146 | + write!(f, " ref")?; |
| 147 | + } |
| 148 | + if column_index + 1 != self.columns.len() { |
| 149 | + write!(f, ",")?; |
| 150 | + } |
| 151 | + write!(f, "\n")?; |
| 152 | + } |
| 153 | + write!(f, ");")?; |
| 154 | + |
| 155 | + Ok(()) |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +impl fmt::Display for Union { |
| 160 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 161 | + write!(f, "@{} = ", self.name)?; |
| 162 | + let mut first = true; |
| 163 | + for member in &self.members { |
| 164 | + if first { |
| 165 | + first = false; |
| 166 | + } else { |
| 167 | + write!(f, " | ")?; |
| 168 | + } |
| 169 | + write!(f, "@{}", member)?; |
| 170 | + } |
| 171 | + Ok(()) |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +/// Generates the dbscheme by writing the given dbscheme `entries` to the `file`. |
| 176 | +pub fn write( |
| 177 | + language_name: &str, |
| 178 | + file: &mut dyn std::io::Write, |
| 179 | + entries: &[Entry], |
| 180 | +) -> std::io::Result<()> { |
| 181 | + write!(file, "// CodeQL database schema for {}\n", language_name)?; |
| 182 | + write!( |
| 183 | + file, |
| 184 | + "// Automatically generated from the tree-sitter grammar; do not edit\n\n" |
| 185 | + )?; |
| 186 | + |
| 187 | + for entry in entries { |
| 188 | + match entry { |
| 189 | + Entry::Table(table) => write!(file, "{}\n\n", table)?, |
| 190 | + Entry::Union(union) => write!(file, "{}\n\n", union)?, |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + Ok(()) |
| 195 | +} |
0 commit comments