mirror of https://github.com/icedland/iced.git
Move nested classes to their own files
This commit is contained in:
parent
94dc2e36b7
commit
d5388ec9bd
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright (C) 2018-2019 de4dot@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Generator.Misc.Python {
|
||||
static class ParseUtils {
|
||||
public static IEnumerable<(string name, string value)> GetArgsNameValues(string argsAttr) {
|
||||
if (!ParseUtils.TryGetArgsPayload(argsAttr, out var args))
|
||||
throw new InvalidOperationException($"Invalid #[args] attr: {argsAttr}");
|
||||
foreach (var part in args.Split(',', StringSplitOptions.RemoveEmptyEntries)) {
|
||||
int index = part.IndexOf('=', StringComparison.Ordinal);
|
||||
if (index < 0)
|
||||
throw new InvalidOperationException();
|
||||
var name = part[..index].Trim();
|
||||
var value = part[(index + 1)..].Trim();
|
||||
yield return (name, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryRemovePrefixSuffix(string s, string prefix, string suffix, [NotNullWhen(true)] out string? extracted) {
|
||||
extracted = null;
|
||||
|
||||
if (!s.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return false;
|
||||
if (!s.EndsWith(suffix, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
extracted = s[prefix.Length..^suffix.Length];
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string[] SplitSphinxTypes(string sphinxType) =>
|
||||
sphinxType.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(a => a.Trim()).ToArray();
|
||||
|
||||
public static bool TryGetSphinxTypeToTypeName(string sphinxType, [NotNullWhen(true)] out string? typeName) =>
|
||||
ParseUtils.TryRemovePrefixSuffix(sphinxType, ":class:`", "`", out typeName);
|
||||
|
||||
public static bool TryGetArgsPayload(string argsAttr, [NotNullWhen(true)] out string? args) =>
|
||||
TryRemovePrefixSuffix(argsAttr, "#[args(", ")]", out args);
|
||||
|
||||
public static bool TryParseTypeAndDocs(string argLine, [NotNullWhen(false)] out string? error, out TypeAndDocs result) {
|
||||
result = default;
|
||||
|
||||
const string pattern = ": ";
|
||||
int index = argLine.IndexOf(pattern, StringComparison.Ordinal);
|
||||
if (index < 0) {
|
||||
error = "Expected `: `";
|
||||
return false;
|
||||
}
|
||||
|
||||
var sphinxType = argLine[..index].Trim();
|
||||
string documentation = argLine[(index + 1)..].Trim();
|
||||
result = new TypeAndDocs(sphinxType, documentation);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
Copyright (C) 2018-2019 de4dot@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Generator.Misc.Python {
|
||||
enum AttributeKind {
|
||||
Ignored,
|
||||
PyClass,
|
||||
PyMethods,
|
||||
PyProto,
|
||||
Derive,
|
||||
New,
|
||||
Getter,
|
||||
Setter,
|
||||
StaticMethod,
|
||||
ClassMethod,
|
||||
TextSignature,
|
||||
Args,
|
||||
}
|
||||
|
||||
sealed class RustAttributes {
|
||||
public readonly List<RustAttribute> Attributes = new List<RustAttribute>();
|
||||
|
||||
public bool Any(params AttributeKind[] attributes) {
|
||||
foreach (var attr in Attributes) {
|
||||
foreach (var kind in attributes) {
|
||||
if (attr.Kind == kind)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{Text,nq}")]
|
||||
sealed class RustAttribute {
|
||||
public readonly AttributeKind Kind;
|
||||
public readonly string Text;
|
||||
|
||||
public RustAttribute(AttributeKind kind, string text) {
|
||||
Kind = kind;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
enum DocCommentKind {
|
||||
Text,
|
||||
Args,
|
||||
Raises,
|
||||
Returns,
|
||||
TestCode,
|
||||
TestOutput,
|
||||
}
|
||||
|
||||
abstract class DocCommentSection {
|
||||
}
|
||||
|
||||
sealed class TextDocCommentSection : DocCommentSection {
|
||||
public readonly string[] Lines;
|
||||
|
||||
public TextDocCommentSection(string[] lines) {
|
||||
int i;
|
||||
for (i = lines.Length - 1; i >= 0; i--) {
|
||||
if (!string.IsNullOrEmpty(lines[i]))
|
||||
break;
|
||||
}
|
||||
if (i + 1 != lines.Length)
|
||||
lines = lines.Take(i).ToArray();
|
||||
Lines = lines;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{SphinxType,nq}: {Documentation,nq}")]
|
||||
readonly struct TypeAndDocs {
|
||||
public readonly string SphinxType;
|
||||
public readonly string Documentation;
|
||||
|
||||
public TypeAndDocs(string sphinxType, string documentation) {
|
||||
SphinxType = sphinxType;
|
||||
Documentation = documentation;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{Name,nq}: {SphinxType}")]
|
||||
readonly struct DocCommentArg {
|
||||
public readonly string Name;
|
||||
public readonly string SphinxType;
|
||||
public readonly string Documentation;
|
||||
|
||||
public DocCommentArg(string name, string sphinxType, string documentation) {
|
||||
Name = name;
|
||||
SphinxType = sphinxType;
|
||||
Documentation = documentation;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ArgsDocCommentSection : DocCommentSection {
|
||||
public readonly DocCommentArg[] Args;
|
||||
public ArgsDocCommentSection(DocCommentArg[] args) => Args = args;
|
||||
}
|
||||
|
||||
sealed class RaisesDocCommentSection : DocCommentSection {
|
||||
public readonly TypeAndDocs[] Raises;
|
||||
public RaisesDocCommentSection(TypeAndDocs[] raises) => Raises = raises;
|
||||
}
|
||||
|
||||
sealed class ReturnsDocCommentSection : DocCommentSection {
|
||||
public readonly TypeAndDocs Returns;
|
||||
public ReturnsDocCommentSection(TypeAndDocs returns) => Returns = returns;
|
||||
}
|
||||
|
||||
sealed class TestCodeDocCommentSection : DocCommentSection {
|
||||
public readonly string[] Lines;
|
||||
public TestCodeDocCommentSection(string[] lines) => Lines = lines;
|
||||
}
|
||||
|
||||
sealed class TestOutputDocCommentSection : DocCommentSection {
|
||||
public readonly string[] Lines;
|
||||
public TestOutputDocCommentSection(string[] lines) => Lines = lines;
|
||||
}
|
||||
|
||||
[DebuggerDisplay("Count = {Sections.Count}")]
|
||||
sealed class DocComments {
|
||||
public readonly List<DocCommentSection> Sections = new List<DocCommentSection>();
|
||||
|
||||
public void AddText(TextDocCommentSection text) {
|
||||
if (text.Lines.Length == 0)
|
||||
return;
|
||||
if (text.Lines.Length == 1 && text.Lines[0] == string.Empty)
|
||||
return;
|
||||
Sections.Add(text);
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{Name,nq}: {RustType,nq}")]
|
||||
readonly struct PyMethodArg {
|
||||
public readonly string Name;
|
||||
public readonly string RustType;
|
||||
public readonly bool IsSelf;
|
||||
|
||||
public PyMethodArg(string name, string rustType, bool isSelf) {
|
||||
Name = name;
|
||||
RustType = rustType;
|
||||
IsSelf = isSelf;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{Name} Args={Arguments.Count}")]
|
||||
sealed class PyMethod {
|
||||
public readonly string Name;
|
||||
public readonly DocComments DocComments;
|
||||
public readonly RustAttributes Attributes;
|
||||
public readonly List<PyMethodArg> Arguments;
|
||||
public readonly string RustReturnType;
|
||||
public bool HasReturnType =>
|
||||
RustReturnType != string.Empty &&
|
||||
RustReturnType != "PyResult<()>";
|
||||
|
||||
public PyMethod(string name, DocComments docComments, RustAttributes attributes, List<PyMethodArg> arguments, string rustReturnType) {
|
||||
Name = name;
|
||||
DocComments = docComments;
|
||||
Attributes = attributes;
|
||||
Arguments = arguments;
|
||||
RustReturnType = rustReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerDisplay("{Name,nq} Methods={Methods.Count}")]
|
||||
sealed class PyClass {
|
||||
public readonly string Name;
|
||||
public readonly DocComments DocComments;
|
||||
public readonly RustAttributes Attributes;
|
||||
public readonly List<PyMethod> Methods;
|
||||
|
||||
public PyClass(string name, DocComments docComments, RustAttributes attributes) {
|
||||
Name = name;
|
||||
DocComments = docComments;
|
||||
Attributes = attributes;
|
||||
Methods = new List<PyMethod>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,938 @@
|
|||
/*
|
||||
Copyright (C) 2018-2019 de4dot@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Generator.Misc.Python {
|
||||
sealed class PyClassParser {
|
||||
const string selfArgName = "$self";
|
||||
readonly string Filename;
|
||||
readonly Lines lines;
|
||||
readonly List<string> DocComments;
|
||||
RustAttributes? Attributes;
|
||||
readonly Dictionary<string, PyClass> pyClasses;
|
||||
|
||||
public PyClassParser(string filename) {
|
||||
Filename = filename;
|
||||
lines = new Lines(File.ReadAllLines(filename));
|
||||
DocComments = new List<string>();
|
||||
pyClasses = new Dictionary<string, PyClass>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
PyClass[] GetClasses() => pyClasses.Values.ToArray();
|
||||
|
||||
bool TryGetPyClass(string name, [NotNullWhen(true)] out PyClass? pyClass) =>
|
||||
pyClasses.TryGetValue(name, out pyClass);
|
||||
|
||||
void AddPyClass(PyClass pyClass) {
|
||||
if (pyClasses.ContainsKey(pyClass.Name))
|
||||
throw GetException($"Duplicate struct {pyClass.Name}");
|
||||
pyClasses.Add(pyClass.Name, pyClass);
|
||||
}
|
||||
|
||||
void ClearTempState() {
|
||||
DocComments.Clear();
|
||||
Attributes = null;
|
||||
}
|
||||
|
||||
bool HasTempState => DocComments.Count != 0 || Attributes is not null;
|
||||
|
||||
public Exception GetException(string message) =>
|
||||
new InvalidOperationException($"{message}, line: {lines.LineNo}, file: {Filename}");
|
||||
|
||||
enum LineKind {
|
||||
Eof,
|
||||
Other,
|
||||
DocComment,
|
||||
Attribute,
|
||||
Struct,
|
||||
Impl,
|
||||
Fn,
|
||||
}
|
||||
|
||||
sealed class Lines {
|
||||
readonly string[] lines;
|
||||
int index;
|
||||
|
||||
public int LineNo => index + 1;
|
||||
|
||||
public Lines(string[] lines) => this.lines = lines;
|
||||
|
||||
public string GetLine(int lineNo) => lines[lineNo - 1];
|
||||
|
||||
public void Skip() {
|
||||
if (index < lines.Length)
|
||||
index++;
|
||||
}
|
||||
|
||||
public (LineKind kind, string line) Next() {
|
||||
var token = Peek();
|
||||
Skip();
|
||||
return token;
|
||||
}
|
||||
|
||||
public (LineKind kind, string line) Peek() {
|
||||
if (index >= lines.Length)
|
||||
return (LineKind.Eof, string.Empty);
|
||||
var line = lines[index];
|
||||
return (GetKind(line), line);
|
||||
}
|
||||
|
||||
static LineKind GetKind(string line) {
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.StartsWith("///", StringComparison.Ordinal))
|
||||
return LineKind.DocComment;
|
||||
if (trimmed.StartsWith("//", StringComparison.Ordinal))
|
||||
return LineKind.Other;
|
||||
if (trimmed.StartsWith("#[", StringComparison.Ordinal))
|
||||
return LineKind.Attribute;
|
||||
if (trimmed.StartsWith("struct ", StringComparison.Ordinal) || trimmed.Contains(" struct ", StringComparison.Ordinal))
|
||||
return LineKind.Struct;
|
||||
if (trimmed.StartsWith("impl ", StringComparison.Ordinal))
|
||||
return LineKind.Impl;
|
||||
if (trimmed.StartsWith("fn ", StringComparison.Ordinal) ||
|
||||
trimmed.StartsWith("pub fn ", StringComparison.Ordinal) ||
|
||||
trimmed.StartsWith("pub(crate) fn ", StringComparison.Ordinal)) {
|
||||
return LineKind.Fn;
|
||||
}
|
||||
return LineKind.Other;
|
||||
}
|
||||
}
|
||||
|
||||
public PyClass[] ParseFile() {
|
||||
while (true) {
|
||||
var token = lines.Next();
|
||||
if (token.kind == LineKind.Eof)
|
||||
break;
|
||||
|
||||
switch (token.kind) {
|
||||
case LineKind.Other:
|
||||
ClearTempState();
|
||||
break;
|
||||
|
||||
case LineKind.DocComment:
|
||||
AddDocCommentLine(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Attribute:
|
||||
ReadAttribute(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Struct:
|
||||
ReadStruct(token.line);
|
||||
SkipBlock(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Impl:
|
||||
if (DocComments.Count != 0)
|
||||
throw GetException("Unexpected doc comments");
|
||||
var implName = GetName(token.line, "impl");
|
||||
if (!TryGetPyClass(implName, out var pyClass) ||
|
||||
Attributes is null ||
|
||||
Attributes.Attributes.Count == 0) {
|
||||
SkipBlock(token.line);
|
||||
}
|
||||
else {
|
||||
if (!Attributes.Any(AttributeKind.PyMethods, AttributeKind.PyProto))
|
||||
SkipBlock(token.line);
|
||||
else
|
||||
ReadStructImpl(token.line, pyClass);
|
||||
}
|
||||
break;
|
||||
|
||||
case LineKind.Fn:
|
||||
SkipBlock(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Eof:
|
||||
default:
|
||||
throw GetException($"Unexpected token {token.kind}");
|
||||
}
|
||||
}
|
||||
|
||||
var classes = GetClasses();
|
||||
foreach (var pyClass in classes) {
|
||||
var pyClassAttr = pyClass.Attributes.Attributes.FirstOrDefault(a => a.Kind == AttributeKind.PyClass);
|
||||
const string expectedPyClassAttr = "#[pyclass(module = \"_iced_x86_py\")]";
|
||||
if (pyClassAttr?.Text != expectedPyClassAttr)
|
||||
throw GetException($"Class {pyClass.Name}: Expected this #[pyclass] attribute: {expectedPyClassAttr}");
|
||||
|
||||
var ctor = pyClass.Methods.FirstOrDefault(a => a.Attributes.Any(AttributeKind.New));
|
||||
int argsSectCount = pyClass.DocComments.Sections.OfType<ArgsDocCommentSection>().Count();
|
||||
int expectedArgsSectCount = (ctor?.Arguments.Count ?? 0) == 0 ? 0 : 1;
|
||||
if (argsSectCount != expectedArgsSectCount)
|
||||
throw GetException($"Class {pyClass.Name}: Expected exactly {expectedArgsSectCount} `Args:` sections but found {argsSectCount}");
|
||||
if (ctor is not null) {
|
||||
var expectedTextSig = GetExpectedTextSignature(ctor);
|
||||
var textSigAttr = pyClass.Attributes.Attributes.First(a => a.Kind == AttributeKind.TextSignature);
|
||||
if (textSigAttr.Text != expectedTextSig)
|
||||
throw GetException($"Class {pyClass.Name}: #[text_signature] didn't match the expected value: {expectedTextSig}");
|
||||
|
||||
var argsAttr = pyClass.Attributes.Attributes.FirstOrDefault(a => a.Kind == AttributeKind.Args);
|
||||
if (argsAttr is not null)
|
||||
throw GetException($"Class {pyClass.Name}: The ctor should have the #[args] attribute");
|
||||
|
||||
if (!CheckArgsSectionAndMethodArgs(pyClass.DocComments, ctor.Arguments, out var error))
|
||||
throw GetException($"Class {pyClass.Name}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
void ReadStruct(string line) {
|
||||
if (Attributes?.Any(AttributeKind.PyClass) == true) {
|
||||
line = RemovePub(line);
|
||||
var name = GetName(line, "struct");
|
||||
if (!TryCreateDocComments(DocComments, out var docComments, out var error))
|
||||
throw GetException(error);
|
||||
var pyClass = new PyClass(name, docComments, Attributes ?? new RustAttributes());
|
||||
AddPyClass(pyClass);
|
||||
}
|
||||
ClearTempState();
|
||||
}
|
||||
|
||||
void ReadStructImpl(string implLine, PyClass pyClass) {
|
||||
ClearTempState();
|
||||
var endOfBlockStr = GetIndent(implLine) + "}";
|
||||
while (true) {
|
||||
var token = lines.Next();
|
||||
if (token.kind == LineKind.Eof)
|
||||
break;
|
||||
if (token.kind == LineKind.Other && token.line == endOfBlockStr)
|
||||
break;
|
||||
|
||||
switch (token.kind) {
|
||||
case LineKind.Other:
|
||||
var trimmed = token.line.Trim();
|
||||
if (!(trimmed == string.Empty || trimmed.StartsWith("//", StringComparison.Ordinal)))
|
||||
throw GetException("Unexpected line");
|
||||
break;
|
||||
|
||||
case LineKind.DocComment:
|
||||
AddDocCommentLine(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Attribute:
|
||||
ReadAttribute(token.line);
|
||||
break;
|
||||
|
||||
case LineKind.Fn:
|
||||
foreach (var method in ReadMethod(pyClass, token.line)) {
|
||||
if (!IgnoreMethod(method))
|
||||
pyClass.Methods.Add(method);
|
||||
}
|
||||
break;
|
||||
|
||||
case LineKind.Struct:
|
||||
case LineKind.Impl:
|
||||
case LineKind.Eof:
|
||||
default:
|
||||
throw GetException($"Unexpected token {token.kind}");
|
||||
}
|
||||
}
|
||||
|
||||
if (HasTempState)
|
||||
throw GetException("Unexpected docs/attrs");
|
||||
}
|
||||
|
||||
static bool IgnoreMethod(PyMethod method) =>
|
||||
method.Name switch {
|
||||
"__traverse__" or "__clear__" or "__next__" => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
string RemovePub(string line) {
|
||||
if (line.StartsWith("pub", StringComparison.Ordinal)) {
|
||||
line = line["pub".Length..];
|
||||
if (line.StartsWith("(", StringComparison.Ordinal)) {
|
||||
int index = line.IndexOf(')');
|
||||
if (index < 0)
|
||||
throw GetException("Expected ')'");
|
||||
line = line[(index + 1)..];
|
||||
}
|
||||
}
|
||||
return line.TrimStart();
|
||||
}
|
||||
|
||||
string ParseMethodArgsAndRetType(string fullLine, string line, bool isInstanceMethod, bool isSpecial, out List<PyMethodArg> args, out string rustReturnType) {
|
||||
args = new List<PyMethodArg>();
|
||||
|
||||
if (!line.StartsWith('('))
|
||||
throw GetException("Expected '('");
|
||||
line = line[1..];
|
||||
bool foundThis = false;
|
||||
int index;
|
||||
while (true) {
|
||||
line = line.Trim();
|
||||
string argsLine;
|
||||
index = line.IndexOf(')');
|
||||
if (index >= 0) {
|
||||
argsLine = line[..index].Trim();
|
||||
line = line[index..].Trim();
|
||||
}
|
||||
else {
|
||||
argsLine = line;
|
||||
line = string.Empty;
|
||||
}
|
||||
foreach (var tmp in argsLine.Split(',', StringSplitOptions.RemoveEmptyEntries)) {
|
||||
var argInfo = tmp.Trim();
|
||||
if (argInfo == string.Empty)
|
||||
continue;
|
||||
const string mutPat = "mut ";
|
||||
if (argInfo.StartsWith(mutPat, StringComparison.Ordinal))
|
||||
argInfo = argInfo[mutPat.Length..].Trim();
|
||||
PyMethodArg arg;
|
||||
switch (argInfo) {
|
||||
case "&mut self":
|
||||
case "&self":
|
||||
if (args.Count != 0)
|
||||
throw GetException("`self` must be the first arg");
|
||||
foundThis = true;
|
||||
arg = new PyMethodArg(selfArgName, argInfo, isSelf: true);
|
||||
break;
|
||||
default:
|
||||
index = argInfo.IndexOf(':');
|
||||
if (index < 0)
|
||||
throw GetException("Expected `:`");
|
||||
var name = argInfo[..index].Trim();
|
||||
var rustType = argInfo[(index + 1)..].Trim();
|
||||
if (rustType.StartsWith("Python<", StringComparison.Ordinal)) {
|
||||
if (name != "py" && name != "_py")
|
||||
throw GetException("Expected name to be `py` or `_py`");
|
||||
continue;
|
||||
}
|
||||
if (name.StartsWith('_')) {
|
||||
if (!isSpecial)
|
||||
throw GetException($"Unused arg {name}");
|
||||
name = name[1..];
|
||||
}
|
||||
bool isSelf = false;
|
||||
if (rustType == "PyRef<Self>" || rustType == "PyRefMut<Self>") {
|
||||
if (args.Count != 0)
|
||||
throw GetException("`self` must be the first arg");
|
||||
foundThis = true;
|
||||
name = selfArgName;
|
||||
isSelf = true;
|
||||
}
|
||||
if (name.Contains(' ', StringComparison.Ordinal))
|
||||
throw GetException("Name has a space");
|
||||
arg = new PyMethodArg(name, rustType, isSelf);
|
||||
break;
|
||||
}
|
||||
args.Add(arg);
|
||||
}
|
||||
|
||||
if (line.StartsWith(')'))
|
||||
break;
|
||||
if (line != string.Empty)
|
||||
throw GetException("Internal error");
|
||||
var token = lines.Next();
|
||||
if (token.kind == LineKind.Eof)
|
||||
throw GetException("Unexpected EOF");
|
||||
fullLine = token.line;
|
||||
line = fullLine.Trim();
|
||||
}
|
||||
if (foundThis != isInstanceMethod) {
|
||||
if (isInstanceMethod)
|
||||
throw GetException("Expected `self` as first arg");
|
||||
throw GetException("`self` found when not an instance method");
|
||||
}
|
||||
|
||||
index = line.IndexOf("->");
|
||||
if (index >= 0) {
|
||||
line = line[(index + 2)..].Trim();
|
||||
index = line.IndexOf('{');
|
||||
if (index < 0)
|
||||
throw GetException("Expected `{`");
|
||||
rustReturnType = line[..index].Trim();
|
||||
line = line[index..];
|
||||
}
|
||||
else
|
||||
rustReturnType = string.Empty;
|
||||
return fullLine;
|
||||
}
|
||||
|
||||
static string GetExpectedTextSignature(PyMethod method) {
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("#[text_signature = \"(");
|
||||
foreach (var arg in method.Arguments) {
|
||||
sb.Append(arg.Name);
|
||||
sb.Append(", ");
|
||||
}
|
||||
sb.Append("/)\"]");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static bool CheckArgsAttribute(PyMethod method, string? argsAttr) {
|
||||
if (argsAttr is null)
|
||||
return true;
|
||||
|
||||
if (!ParseUtils.TryGetArgsPayload(argsAttr, out var s))
|
||||
return false;
|
||||
var attrArgs = s.Split(',');
|
||||
if (attrArgs.Length > method.Arguments.Count)
|
||||
return false;
|
||||
for (int i = 0; i < attrArgs.Length; i++) {
|
||||
var attrArg = attrArgs[i];
|
||||
int index = attrArg.IndexOf('=', StringComparison.Ordinal);
|
||||
if (index < 0)
|
||||
return false;
|
||||
var attrArgName = attrArg[..index].Trim();
|
||||
var argName = method.Arguments[method.Arguments.Count - attrArgs.Length + i].Name;
|
||||
if (attrArgName != argName)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CheckArgsSectionAndMethodArgs(DocComments docComments, List<PyMethodArg> arguments, [NotNullWhen(false)] out string? error) {
|
||||
var argsSection = docComments.Sections.OfType<ArgsDocCommentSection>().FirstOrDefault();
|
||||
if (arguments.Count == 0) {
|
||||
if (argsSection is not null) {
|
||||
error = "Unexpected `Args:` section";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
int argsSectionLength = argsSection?.Args.Length ?? 0;
|
||||
int hasThis = arguments.Count != 0 && arguments[0].IsSelf ? 1 : 0;
|
||||
int expectedMethodArgs = arguments.Count - hasThis;
|
||||
if (argsSectionLength != expectedMethodArgs) {
|
||||
error = $"Expected `Args:` section with {expectedMethodArgs} but found {argsSectionLength} documented args";
|
||||
return false;
|
||||
}
|
||||
for (int i = hasThis; i < arguments.Count; i++) {
|
||||
var methodArg = arguments[i].Name;
|
||||
var argsArg = argsSection!.Args[i - hasThis].Name;
|
||||
if (methodArg != argsArg) {
|
||||
error = $"`Args:` section not sorted or using the wrong name. Expected `{methodArg}` but found `{argsArg}`";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
IEnumerable<PyMethod> ReadMethod(PyClass pyClass, string fullLine) {
|
||||
var firstLine = fullLine;
|
||||
var line = RemovePub(fullLine.Trim());
|
||||
const string fnPat = "fn ";
|
||||
if (!line.StartsWith(fnPat, StringComparison.Ordinal))
|
||||
throw GetException("Expected `fn`");
|
||||
line = line[fnPat.Length..].Trim();
|
||||
|
||||
int index = line.IndexOf('(');
|
||||
if (index < 0)
|
||||
throw GetException("Expected `(`");
|
||||
var name = line[..index].Trim();
|
||||
line = line[index..];
|
||||
index = name.IndexOf('<', StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
name = name[..index].Trim();
|
||||
|
||||
var attributes = Attributes ?? new RustAttributes();
|
||||
bool isStaticMethod = attributes.Any(AttributeKind.StaticMethod) == true;
|
||||
bool isClassMethod = attributes.Any(AttributeKind.ClassMethod) == true;
|
||||
bool isCtor = attributes.Any(AttributeKind.New) == true;
|
||||
bool isInstanceMethod = !isStaticMethod && !isClassMethod && !isCtor;
|
||||
if (isStaticMethod && isClassMethod)
|
||||
throw GetException("Method can't be both classmethod and staticmethod");
|
||||
|
||||
bool isSpecial = name.StartsWith("__", StringComparison.Ordinal) &&
|
||||
name.EndsWith("__", StringComparison.Ordinal) &&
|
||||
name != "__copy__" && name != "__deepcopy__";
|
||||
|
||||
fullLine = ParseMethodArgsAndRetType(fullLine, line, isInstanceMethod, isSpecial || name == "__deepcopy__", out var args, out var rustReturnType);
|
||||
if (!TryCreateDocComments(DocComments, out var docComments, out var error))
|
||||
throw GetException(error);
|
||||
|
||||
bool isSetter = attributes.Any(AttributeKind.Setter) == true;
|
||||
bool isGetter = attributes.Any(AttributeKind.Getter) == true;
|
||||
|
||||
if (isSetter && name.StartsWith("set_"))
|
||||
name = name["set_".Length..];
|
||||
if (isGetter && name.StartsWith("get_"))
|
||||
throw GetException($"Getters shouldn't have a `get_` prefix: {name}");
|
||||
|
||||
var method = new PyMethod(name, docComments, attributes, args, rustReturnType);
|
||||
|
||||
var argsAttr = method.Attributes.Attributes.FirstOrDefault(a => a.Kind == AttributeKind.Args);
|
||||
if (isSpecial || isGetter || isSetter) {
|
||||
if (argsAttr is not null)
|
||||
throw GetException("Unexpected #[args] attribute found");
|
||||
}
|
||||
else {
|
||||
if (!CheckArgsAttribute(method, argsAttr?.Text))
|
||||
throw GetException($"Invalid #[args] attribute: {argsAttr?.Text}");
|
||||
}
|
||||
|
||||
if (!(isSpecial || isGetter || isSetter || isCtor)) {
|
||||
int count = method.Attributes.Attributes.Count(a => a.Kind == AttributeKind.TextSignature);
|
||||
if (count != 1)
|
||||
throw GetException("Expected exactly one #[text_signature] attribute");
|
||||
var expectedTextSig = GetExpectedTextSignature(method);
|
||||
var textSigAttr = method.Attributes.Attributes.First(a => a.Kind == AttributeKind.TextSignature);
|
||||
if (textSigAttr.Text != expectedTextSig)
|
||||
throw GetException($"#[text_signature] didn't match the expected value: {expectedTextSig}");
|
||||
}
|
||||
|
||||
if (isSetter) {
|
||||
if (method.DocComments.Sections.Count > 0)
|
||||
throw GetException($"Setters should have no docs, only getters should have docs: {name}");
|
||||
const string setterArgName = "new_value";
|
||||
if (method.Arguments.Count != 2)
|
||||
throw GetException($"Invalid number of setter arguments, expected 2 but found {method.Arguments.Count}");
|
||||
if (method.Arguments[1].Name != setterArgName)
|
||||
throw GetException($"Setter argument name must be `{setterArgName}` not {method.Arguments[1].Name}");
|
||||
}
|
||||
else {
|
||||
if (method.DocComments.Sections.OfType<ArgsDocCommentSection>().Count() > 1)
|
||||
throw GetException("Too many `Args:` sections");
|
||||
if (method.DocComments.Sections.OfType<RaisesDocCommentSection>().Count() > 1)
|
||||
throw GetException("Too many `Raises:` sections");
|
||||
if (isGetter) {
|
||||
if (method.DocComments.Sections.OfType<ReturnsDocCommentSection>().Any())
|
||||
throw GetException("Setters should have no `Returns:` sections. The return type should be the first type on the first doc line, eg. `int: Some docs here`");
|
||||
if (method.DocComments.Sections.FirstOrDefault() is not TextDocCommentSection sect || sect.Lines.Length == 0)
|
||||
throw GetException("Expected first doc comments section to be text");
|
||||
if (!ParseUtils.TryParseTypeAndDocs(sect.Lines[0], out _, out _))
|
||||
throw GetException("First data on the first line must be the property type");
|
||||
}
|
||||
else if (isCtor) {
|
||||
if (method.DocComments.Sections.Count != 0)
|
||||
throw GetException("Ctors should have no docs, they should be part of the class' docs");
|
||||
}
|
||||
else {
|
||||
if (!isSpecial && method.DocComments.Sections.Count == 0)
|
||||
throw GetException($"Missing documentation: {name}");
|
||||
if (!isSpecial && method.HasReturnType) {
|
||||
if (method.DocComments.Sections.OfType<ReturnsDocCommentSection>().Count() != 1)
|
||||
throw GetException("Expected exactly one `Returns:` section");
|
||||
}
|
||||
else {
|
||||
if (method.DocComments.Sections.OfType<ReturnsDocCommentSection>().Any())
|
||||
throw GetException("Expected no `Returns:` sections");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSpecial && !isGetter && !isSetter && !isCtor) {
|
||||
if (!CheckArgsSectionAndMethodArgs(method.DocComments, method.Arguments, out error))
|
||||
throw GetException(error);
|
||||
}
|
||||
|
||||
var (startLine, endLine) = SkipBlock(fullLine);
|
||||
ClearTempState();
|
||||
|
||||
if (method.Name == "__richcmp__") {
|
||||
var seenCompareOps = new HashSet<CompareOp>();
|
||||
for (int lineNo = startLine; lineNo < endLine; lineNo++) {
|
||||
line = lines.GetLine(lineNo);
|
||||
foreach (var compareOp in GetCompareOps(line)) {
|
||||
if (!seenCompareOps.Add(compareOp))
|
||||
throw GetException("Duplicate CompareOp found in the method");
|
||||
var opName = compareOp switch {
|
||||
CompareOp.Lt => "__lt__",
|
||||
CompareOp.Le => "__le__",
|
||||
CompareOp.Eq => "__eq__",
|
||||
CompareOp.Ne => "__ne__",
|
||||
CompareOp.Gt => "__gt__",
|
||||
CompareOp.Ge => "__ge__",
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
var newArgs = new List<PyMethodArg> {
|
||||
new PyMethodArg(selfArgName, "&self", isSelf: true),
|
||||
new PyMethodArg("other", "&PyAny", isSelf: false),
|
||||
};
|
||||
yield return new PyMethod(opName, method.DocComments, method.Attributes, newArgs, "bool");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
yield return method;
|
||||
}
|
||||
|
||||
enum CompareOp {
|
||||
Lt,
|
||||
Le,
|
||||
Eq,
|
||||
Ne,
|
||||
Gt,
|
||||
Ge,
|
||||
}
|
||||
|
||||
static IEnumerable<CompareOp> GetCompareOps(string line) {
|
||||
if (line.Contains("CompareOp::Lt", StringComparison.Ordinal)) yield return CompareOp.Lt;
|
||||
if (line.Contains("CompareOp::Le", StringComparison.Ordinal)) yield return CompareOp.Le;
|
||||
if (line.Contains("CompareOp::Eq", StringComparison.Ordinal)) yield return CompareOp.Eq;
|
||||
if (line.Contains("CompareOp::Ne", StringComparison.Ordinal)) yield return CompareOp.Ne;
|
||||
if (line.Contains("CompareOp::Gt", StringComparison.Ordinal)) yield return CompareOp.Gt;
|
||||
if (line.Contains("CompareOp::Ge", StringComparison.Ordinal)) yield return CompareOp.Ge;
|
||||
}
|
||||
|
||||
void AddDocCommentLine(string line) {
|
||||
line = line.TrimStart();
|
||||
const string DocCommentPrefix = "///";
|
||||
if (!line.StartsWith(DocCommentPrefix, StringComparison.Ordinal))
|
||||
throw GetException("Expected a doc comment");
|
||||
var docComment = line[DocCommentPrefix.Length..];
|
||||
if (docComment.StartsWith(" ", StringComparison.Ordinal))
|
||||
docComment = docComment[1..];
|
||||
DocComments.Add(docComment);
|
||||
}
|
||||
|
||||
void ReadAttribute(string line) {
|
||||
Attributes ??= new RustAttributes();
|
||||
Attributes.Attributes.Add(ParseAttribute(line));
|
||||
}
|
||||
|
||||
RustAttribute ParseAttribute(string line) {
|
||||
var attrLine = line.Trim();
|
||||
var fullAttrLine = attrLine;
|
||||
const string attrPrefix = "#[";
|
||||
if (!attrLine.StartsWith(attrPrefix, StringComparison.Ordinal))
|
||||
throw GetException("Expected an attribute");
|
||||
attrLine = attrLine[attrPrefix.Length..];
|
||||
int index = attrLine.IndexOfAny(new[] { '(', ' ', '=', ']' });
|
||||
if (index < 0)
|
||||
throw GetException("Invalid attribute");
|
||||
var attrName = attrLine.Substring(0, index);
|
||||
var attrKind = attrName switch {
|
||||
"pyclass" => AttributeKind.PyClass,
|
||||
"pymethods" => AttributeKind.PyMethods,
|
||||
"pyproto" => AttributeKind.PyProto,
|
||||
"new" => AttributeKind.New,
|
||||
"getter" => AttributeKind.Getter,
|
||||
"setter" => AttributeKind.Setter,
|
||||
"staticmethod" => AttributeKind.StaticMethod,
|
||||
"classmethod" => AttributeKind.ClassMethod,
|
||||
"text_signature" => AttributeKind.TextSignature,
|
||||
"args" => AttributeKind.Args,
|
||||
"derive" or "allow" or "rustfmt::skip" or "macro_use" or
|
||||
"pymodule" or "inline" => AttributeKind.Ignored,
|
||||
// Don't ignore unknown attrs by default. We must know what the attribute does
|
||||
// so we don't ignore an important one that gets added in the future.
|
||||
_ => throw GetException($"Unknown attribute: `{attrName}`"),
|
||||
};
|
||||
return new RustAttribute(attrKind, fullAttrLine);
|
||||
}
|
||||
|
||||
string GetName(string line, string keyword) {
|
||||
int index = line.IndexOf(keyword + " ", StringComparison.Ordinal);
|
||||
if (index < 0)
|
||||
throw GetException($"Expected `{keyword}`");
|
||||
var name = line[(index + keyword.Length + 1)..].Trim();
|
||||
index = name.IndexOf('{');
|
||||
if (index < 0)
|
||||
index = name.Length;
|
||||
name = name.Substring(0, index);
|
||||
const string forString = " for ";
|
||||
index = name.IndexOf(forString);
|
||||
if (index >= 0)
|
||||
name = name[(index + forString.Length)..];
|
||||
name = name.Trim();
|
||||
if (name.Contains(' ', StringComparison.Ordinal))
|
||||
throw GetException($"Found whitespace in `id` after keyword `{keyword}`");
|
||||
return name;
|
||||
}
|
||||
|
||||
static string GetIndent(string line) {
|
||||
var trimmed = line.TrimStart();
|
||||
return line.Substring(0, line.IndexOf(trimmed, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
(int startLine, int endLine) SkipBlock(string line) {
|
||||
if (line.EndsWith(';') || line.EndsWith('}')) {
|
||||
ClearTempState();
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
var expectedIndent = GetIndent(line);
|
||||
var expected = expectedIndent + "}";
|
||||
bool seenOpeningBlock = line.EndsWith("{", StringComparison.Ordinal);
|
||||
var startLine = lines.LineNo;
|
||||
while (true) {
|
||||
var token = lines.Next();
|
||||
if (token.kind == LineKind.Eof)
|
||||
throw GetException("Unexpected EOF");
|
||||
if (token.line != string.Empty) {
|
||||
var indent = GetIndent(token.line);
|
||||
if (indent.Length < expectedIndent.Length)
|
||||
throw GetException("New indent < current indent");
|
||||
if (indent.Length == expectedIndent.Length) {
|
||||
if (!seenOpeningBlock &&
|
||||
token.line.EndsWith("{", StringComparison.Ordinal) &&
|
||||
expectedIndent == GetIndent(token.line)) {
|
||||
seenOpeningBlock = true;
|
||||
}
|
||||
else {
|
||||
if (expected != token.line)
|
||||
throw GetException("Expected end of block");
|
||||
if (!seenOpeningBlock)
|
||||
throw GetException("Missing `{` at the start of the block");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ClearTempState();
|
||||
return (startLine, lines.LineNo - 1);
|
||||
}
|
||||
|
||||
static DocCommentKind GetDocCommentKind(string line) =>
|
||||
line switch {
|
||||
"Args:" => DocCommentKind.Args,
|
||||
"Raises:" => DocCommentKind.Raises,
|
||||
"Returns:" => DocCommentKind.Returns,
|
||||
".. testcode::" => DocCommentKind.TestCode,
|
||||
".. testoutput::" => DocCommentKind.TestOutput,
|
||||
_ => DocCommentKind.Text,
|
||||
};
|
||||
|
||||
static bool TryCreateDocComments(List<string> lines, [NotNullWhen(true)] out DocComments? docComments, [NotNullWhen(false)] out string? error) {
|
||||
docComments = null;
|
||||
|
||||
var docs = new DocComments();
|
||||
var currentTextLines = new List<string>();
|
||||
for (int i = 0; i < lines.Count; i++) {
|
||||
var line = lines[i];
|
||||
if (line == string.Empty) {
|
||||
AddCurrentText(docs, currentTextLines);
|
||||
continue;
|
||||
}
|
||||
|
||||
var kind = GetDocCommentKind(line);
|
||||
if (kind != DocCommentKind.Text && currentTextLines.Count != 0)
|
||||
AddCurrentText(docs, currentTextLines);
|
||||
switch (kind) {
|
||||
case DocCommentKind.Text:
|
||||
currentTextLines.Add(line);
|
||||
break;
|
||||
|
||||
case DocCommentKind.Args:
|
||||
var args = new List<DocCommentArg>();
|
||||
if (!TryGetDescLines(lines, ref i, out var descLines, out error))
|
||||
return false;
|
||||
foreach (var argLine in descLines) {
|
||||
if (!TryParseDocCommentArg(argLine, out error, out var arg))
|
||||
return false;
|
||||
args.Add(arg);
|
||||
}
|
||||
if (args.Count == 0) {
|
||||
error = "Missing `Args` info lines";
|
||||
return false;
|
||||
}
|
||||
docs.Sections.Add(new ArgsDocCommentSection(args.ToArray()));
|
||||
break;
|
||||
|
||||
case DocCommentKind.Raises:
|
||||
var raises = new List<TypeAndDocs>();
|
||||
if (!TryGetDescLines(lines, ref i, out descLines, out error))
|
||||
return false;
|
||||
foreach (var argLine in descLines) {
|
||||
if (!ParseUtils.TryParseTypeAndDocs(argLine, out error, out var raisesInfo))
|
||||
return false;
|
||||
raises.Add(raisesInfo);
|
||||
}
|
||||
if (raises.Count == 0) {
|
||||
error = "Missing `Raises` info lines";
|
||||
return false;
|
||||
}
|
||||
docs.Sections.Add(new RaisesDocCommentSection(raises.ToArray()));
|
||||
break;
|
||||
|
||||
case DocCommentKind.Returns:
|
||||
TypeAndDocs? returns = null;
|
||||
if (!TryGetDescLines(lines, ref i, out descLines, out error))
|
||||
return false;
|
||||
foreach (var argLine in descLines) {
|
||||
if (returns is not null) {
|
||||
error = "Multiple `Returns` info lines";
|
||||
return false;
|
||||
}
|
||||
if (!ParseUtils.TryParseTypeAndDocs(argLine, out error, out var returnsTmp))
|
||||
return false;
|
||||
returns = returnsTmp;
|
||||
}
|
||||
if (returns is null) {
|
||||
error = "Missing `Returns` info lines";
|
||||
return false;
|
||||
}
|
||||
docs.Sections.Add(new ReturnsDocCommentSection(returns.Value));
|
||||
break;
|
||||
|
||||
case DocCommentKind.TestCode:
|
||||
if (!TryReadExampleLines(lines, ref i, out var exampleLines, out error))
|
||||
return false;
|
||||
docs.Sections.Add(new TestCodeDocCommentSection(exampleLines));
|
||||
break;
|
||||
|
||||
case DocCommentKind.TestOutput:
|
||||
if (!TryReadExampleLines(lines, ref i, out var outputLines, out error))
|
||||
return false;
|
||||
docs.Sections.Add(new TestOutputDocCommentSection(outputLines));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
AddCurrentText(docs, currentTextLines);
|
||||
|
||||
docComments = docs;
|
||||
error = null;
|
||||
return true;
|
||||
|
||||
static void AddCurrentText(DocComments docs, List<string> textLines) {
|
||||
if (textLines.Count != 0) {
|
||||
docs.AddText(new TextDocCommentSection(textLines.ToArray()));
|
||||
textLines.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool TryReadExampleLines(List<string> lines, ref int i, [NotNullWhen(true)] out string[]? resultLines, [NotNullWhen(false)] out string? error) {
|
||||
resultLines = null;
|
||||
|
||||
if (!lines[i].StartsWith(".. ", StringComparison.Ordinal)) {
|
||||
error = "Expected `.. `";
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
if (i >= lines.Count) {
|
||||
error = "Missing example/output lines";
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(lines[i])) {
|
||||
error = "First example/output line must be an empty line";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool lastLineWasEmpty = false;
|
||||
var result = new List<string>();
|
||||
for (; i + 1 < lines.Count; i++) {
|
||||
var line = lines[i + 1];
|
||||
|
||||
if (line == string.Empty)
|
||||
lastLineWasEmpty = true;
|
||||
else {
|
||||
const string indent = " ";
|
||||
if (!line.StartsWith(indent, StringComparison.Ordinal)) {
|
||||
if (lastLineWasEmpty)
|
||||
break;
|
||||
error = $"Expected indented text: `/// {indent}text here`";
|
||||
return false;
|
||||
}
|
||||
line = line[indent.Length..];
|
||||
lastLineWasEmpty = false;
|
||||
}
|
||||
result.Add(line);
|
||||
}
|
||||
|
||||
while (result.Count > 0 && result[^1] == string.Empty)
|
||||
result.RemoveAt(result.Count - 1);
|
||||
if (result.Count == 0) {
|
||||
error = "Missing example/output lines";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultLines = result.ToArray();
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryParseDocCommentArg(string argLine, [NotNullWhen(false)] out string? error, out DocCommentArg arg) {
|
||||
arg = default;
|
||||
int index = argLine.IndexOf(' ');
|
||||
if (index < 0) {
|
||||
error = "Expected ' '";
|
||||
return false;
|
||||
}
|
||||
var name = argLine[..index];
|
||||
if (name.StartsWith('`')) {
|
||||
if (!name.EndsWith('`')) {
|
||||
error = "Expected ' '";
|
||||
return false;
|
||||
}
|
||||
name = name[1..^1];
|
||||
}
|
||||
if (name.Contains(' ')) {
|
||||
error = $"Found a space in the name: `{name}`";
|
||||
return false;
|
||||
}
|
||||
|
||||
argLine = argLine[index..];
|
||||
index = argLine.IndexOf('(', StringComparison.Ordinal);
|
||||
if (index < 0) {
|
||||
error = "Expected '('";
|
||||
return false;
|
||||
}
|
||||
const string pattern = "):";
|
||||
int endIndex = argLine.IndexOf(pattern, StringComparison.Ordinal);
|
||||
if (endIndex < 0) {
|
||||
error = $"Expected '{pattern}'";
|
||||
return false;
|
||||
}
|
||||
var sphinxType = argLine[(index + 1)..endIndex];
|
||||
var docs = argLine[(endIndex + pattern.Length)..].Trim();
|
||||
|
||||
arg = new DocCommentArg(name, sphinxType, docs);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryGetDescLines(List<string> lines, ref int index, [NotNullWhen(true)] out List<string>? result, [NotNullWhen(false)] out string? error) {
|
||||
result = null;
|
||||
|
||||
var descLines = new List<string>();
|
||||
string? expectedIndent = null;
|
||||
while (index + 1 < lines.Count) {
|
||||
var line = lines[index + 1];
|
||||
if (!line.StartsWith(' '))
|
||||
break;
|
||||
if (expectedIndent is null)
|
||||
expectedIndent = GetIndent(line);
|
||||
var indent = GetIndent(line);
|
||||
if (indent != expectedIndent) {
|
||||
error = "Invalid indent";
|
||||
return false;
|
||||
}
|
||||
descLines.Add(line.TrimStart());
|
||||
index++;
|
||||
}
|
||||
result = descLines;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue