1f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)package org.chromium.devtools.jsdoc.checks;
2f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
3f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import com.google.common.base.Joiner;
4197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.jscomp.NodeUtil;
5197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.JSDocInfo;
6197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.Node;
7197021e6b966cfb06891637935ef33fff06433d1Ben Murdochimport com.google.javascript.rhino.Token;
8f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
9f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.HashSet;
10f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.Set;
11f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.regex.Matcher;
12f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)import java.util.regex.Pattern;
13f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
14f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)public final class MethodAnnotationChecker extends ContextTrackingChecker {
15f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
16f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private static final Pattern PARAM_PATTERN =
17f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            Pattern.compile("^[^@\n]*@param\\s+(\\{.+\\}\\s+)?([^\\s]+).*$", Pattern.MULTILINE);
18f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
19f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private static final Pattern INVALID_RETURN_PATTERN =
20f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            Pattern.compile("^[^@\n]*(@)return(?:s.*|\\s+[^{]*)$", Pattern.MULTILINE);
21f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
22f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private final Set<FunctionRecord> valueReturningFunctions = new HashSet<>();
23f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private final Set<FunctionRecord> throwingFunctions = new HashSet<>();
24f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
25f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    @Override
26197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    public void enterNode(Node node) {
27f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        switch (node.getType()) {
28f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        case Token.FUNCTION:
29197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            handleFunction(node);
30f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            break;
31f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        case Token.RETURN:
32197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            handleReturn(node);
33f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            break;
34f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        case Token.THROW:
35f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            handleThrow();
36f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            break;
37f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        default:
38f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            break;
39f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
40f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
41f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
42197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private void handleFunction(Node functionNode) {
43197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Node parametersNode = NodeUtil.getFunctionParameters(functionNode);
44197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        int actualParamCount = parametersNode.getChildCount();
45f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (actualParamCount == 0) {
46f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
47f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
48197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        JSDocInfo jsDocInfo = NodeUtil.getBestJSDocInfo(functionNode);
49197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        String[] nonAnnotatedParams = getNonAnnotatedParamData(parametersNode, jsDocInfo);
50197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        if (nonAnnotatedParams.length > 0 && actualParamCount != nonAnnotatedParams.length) {
51197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            reportErrorAtOffset(jsDocInfo.getOriginalCommentPosition(),
52197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                    String.format(
53197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                            "No @param JSDoc tag found for parameters: [%s]",
54197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                            Joiner.on(',').join(nonAnnotatedParams)));
55f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
56f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
57f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
58197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private String[] getNonAnnotatedParamData(Node params, JSDocInfo info) {
59197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        if (info == null) {
60f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return new String[0];
61f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
62197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Set<String> formalParamNames = new HashSet<>();
63197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        for (int i = 0, childCount = params.getChildCount(); i < childCount; ++i) {
64197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            Node paramNode = params.getChildAtIndex(i);
65f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            String paramName = getContext().getNodeText(paramNode);
66197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            if (!formalParamNames.add(paramName)) {
67f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                reportErrorAtNodeStart(paramNode,
68f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                        String.format("Duplicate function argument name: %s", paramName));
69f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            }
70f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
71197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Matcher m = PARAM_PATTERN.matcher(info.getOriginalCommentString());
72f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        while (m.find()) {
73f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            String paramType = m.group(1);
74f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            if (paramType == null) {
75197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                reportErrorAtOffset(info.getOriginalCommentPosition() + m.start(2),
76197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                        String.format(
77197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                                "Invalid @param annotation found -"
78197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                                + " should be \"@param {<type>} paramName\""));
79f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            } else {
80197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                formalParamNames.remove(m.group(2));
81f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            }
82f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
83197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        return formalParamNames.toArray(new String[formalParamNames.size()]);
84f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
85f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
86197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private void handleReturn(Node node) {
87197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        if (node.getFirstChild() == null || AstUtil.parentOfType(node, Token.ASSIGN) != null) {
88f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
89f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
90f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
91f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        FunctionRecord record = getState().getCurrentFunctionRecord();
92f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (record == null) {
93f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
94f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
95197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Node nameNode = getFunctionNameNode(record.functionNode);
96f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (nameNode == null) {
97f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
98f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
99f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        valueReturningFunctions.add(record);
100f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
101f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
102f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private void handleThrow() {
103f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        FunctionRecord record = getState().getCurrentFunctionRecord();
104f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (record == null) {
105f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
106f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
107197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Node nameNode = getFunctionNameNode(record.functionNode);
108f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (nameNode == null) {
109f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
110f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
111f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        throwingFunctions.add(record);
112f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
113f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
114f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    @Override
115197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    public void leaveNode(Node node) {
116f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (node.getType() != Token.FUNCTION) {
117f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
118f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
119f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
120f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        FunctionRecord record = getState().getCurrentFunctionRecord();
121f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (record != null) {
122f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            checkFunctionAnnotation(record);
123f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
124f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
125f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
126f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    @SuppressWarnings("unused")
127f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private void checkFunctionAnnotation(FunctionRecord function) {
128f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        String functionName = getFunctionName(function.functionNode);
129f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (functionName == null) {
130f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
131f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
132197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        String[] parts = functionName.split("\\.");
133197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        functionName = parts[parts.length - 1];
134f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        boolean isApiFunction = !functionName.startsWith("_")
135f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                && (function.isTopLevelFunction()
136f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                        || (function.enclosingType != null
137f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                                && isPlainTopLevelFunction(function.enclosingFunctionRecord)));
138f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
139f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        boolean isReturningFunction = valueReturningFunctions.contains(function);
140f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        boolean isInterfaceFunction =
141197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                function.enclosingType != null && function.enclosingType.isInterface();
142f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        int invalidAnnotationIndex =
143197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                invalidReturnAnnotationIndex(function.info);
144f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (invalidAnnotationIndex != -1) {
145f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            String suggestedResolution = (isReturningFunction || isInterfaceFunction)
146f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                    ? "should be \"@return {<type>}\""
147f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                    : "please remove, as function does not return value";
148197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            getContext().reportErrorAtOffset(
149197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                    function.info.getOriginalCommentPosition() + invalidAnnotationIndex,
150f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                    String.format(
151f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                            "invalid return type annotation found - %s", suggestedResolution));
152f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
153f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
154197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Node functionNameNode = getFunctionNameNode(function.functionNode);
155f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (functionNameNode == null) {
156f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return;
157f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
158f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
159f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        if (isReturningFunction) {
160197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch            if (!function.isConstructor() && !function.hasReturnAnnotation() && isApiFunction) {
161f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                reportErrorAtNodeStart(
162f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                        functionNameNode,
163f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                        "@return annotation is required for API functions that return value");
164f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            }
165f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        } else {
166f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            // A @return-function that does not actually return anything and
167f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            // is intended to be overridden in subclasses must throw.
168f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            if (function.hasReturnAnnotation()
169f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                    && !isInterfaceFunction
170f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                    && !throwingFunctions.contains(function)) {
171f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                reportErrorAtNodeStart(functionNameNode,
172f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)                        "@return annotation found, yet function does not return value");
173f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            }
174f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
175f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
176f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
177f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    private static boolean isPlainTopLevelFunction(FunctionRecord record) {
178f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        return record != null && record.isTopLevelFunction()
179197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch                && (record.enclosingType == null && !record.isConstructor());
180f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
181f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
182197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private String getFunctionName(Node functionNode) {
183197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Node nameNode = getFunctionNameNode(functionNode);
184f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        return nameNode == null ? null : getState().getNodeText(nameNode);
185f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
186f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
187197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private static int invalidReturnAnnotationIndex(JSDocInfo info) {
188197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        if (info == null) {
189f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)            return -1;
190f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        }
191197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        Matcher m = INVALID_RETURN_PATTERN.matcher(info.getOriginalCommentString());
192f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)        return m.find() ? m.start(1) : -1;
193f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
194f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)
195197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    private static Node getFunctionNameNode(Node functionNode) {
196197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        // FIXME: Do not require annotation for assignment-RHS functions.
197197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch        return AstUtil.getFunctionNameNode(functionNode);
198f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)    }
199f6b7aed3f7ce69aca0d7a032d144cbd088b04393Torne (Richard Coles)}
200