Future followsTemplate()

in lib/src/report/template.dart [20:318]


Future<ReportSection> followsTemplate(PackageContext context) async {
  final options = context.options;
  final packageDir = context.packageDir;
  final pubspec = context.pubspec;

  Future<List<Issue>> findUrlIssues(
    String key,
    String name, {
    bool isRequired = false,
  }) async {
    final content = pubspec.originalYaml[key];
    if (content != null && content is! String) {
      return [
        Issue(
            'The `$key` entry, if present, should be a string containing a url',
            span: tryGetSpanFromYamlMap(pubspec.originalYaml, key))
      ];
    }
    final url = content as String?;
    final issues = <Issue>[];

    if (url == null || url.isEmpty) {
      if (isRequired) {
        issues.add(
          Issue("`pubspec.yaml` doesn't have a `$key` entry."),
        );
      }
      return issues;
    }

    final status = await context.urlChecker.checkStatus(url);
    if (status.isInvalid) {
      issues.add(
        Issue(
          "$name isn't valid.",
          span: tryGetSpanFromYamlMap(pubspec.originalYaml, key),
        ),
      );
    } else if (status.isInternal && !options.isInternal) {
      issues.add(
        Issue(
          "$name isn't helpful.",
          span: tryGetSpanFromYamlMap(pubspec.originalYaml, key),
        ),
      );
    } else if (!status.exists) {
      issues.add(
        Issue(
          "$name doesn't exist.",
          span: tryGetSpanFromYamlMap(pubspec.originalYaml, key),
          suggestion: 'At the time of the analysis `$url` was unreachable.',
        ),
      );
    } else if (!status.isSecure) {
      issues.add(
        Issue(
          '$name is insecure.',
          span: tryGetSpanFromYamlMap(pubspec.originalYaml, key),
          suggestion: 'Update the `$key` field and use a secure (`https`) URL.',
        ),
      );
    }
    final problemCode =
        status.getProblemCode(packageIsKnownInternal: options.isInternal);
    if (problemCode != null) {
      context.urlProblems[url] = problemCode;
    }
    return issues;
  }

  List<Issue> findFileSizeIssues(File file,
      {int limitInKB = 128, String? missingSuggestion}) {
    final length = file.lengthSync();
    final lengthInKB = length / 1024.0;
    return [
      if (length == 0)
        Issue('${p.relative(file.path, from: packageDir)} is empty.',
            suggestion: missingSuggestion),
      if (lengthInKB > limitInKB)
        Issue(
          '${p.relative(file.path, from: packageDir)} too large.',
          suggestion: 'Try to keep the file size under ${limitInKB}k.',
        )
    ];
  }

  /// Analyze a markdown file and return suggestions.
  Future<List<Issue>> findMarkdownIssues(File file) async {
    final issues = <Issue>[];
    final filename = p.basename(file.path);
    final analysis = await scanMarkdownFileContent(file);
    Future<void> findLinkIssues(List<Link> links, String linkType) async {
      final checked = await checkLinks(links);
      // TODO: warn about relative image URLs
      // TODO: warn about relative links
      // TODO: consider checking whether the URL exists and returns HTTP 200.

      if (checked.unparsed.isNotEmpty) {
        final count = checked.unparsed.length;
        final first = checked.unparsed.first;
        final s = count == 1 ? '' : 's';
        issues.add(Issue(
            'Links in `$filename` should be well formed '
            'Unable to parse $count image link$s.',
            span: first.span));
      }
      if (checked.insecure.isNotEmpty) {
        final count = checked.insecure.length;
        final first = checked.insecure.first;
        final sAre = count == 1 ? ' is' : 's are';
        issues.add(Issue(
            'Links in `$filename` should be secure. $count $linkType$sAre insecure.',
            suggestion: 'Use `https` URLs instead.',
            span: first.span));
      }
    }

    await findLinkIssues(analysis.links, 'link');
    await findLinkIssues(analysis.images, 'image link');
    if (analysis.isMalformedUtf8) {
      issues.add(Issue(
        '`$filename` is not a valid UTF-8 file.',
        suggestion:
            'The content of `$filename` in your package should contain valid UTF-8 characters.',
      ));
    }
    if (analysis.nonAsciiRatio > 0.2) {
      issues.add(Issue(
        '`$filename` contains too many non-ASCII characters.',
        suggestion:
            'The site uses English as its primary language. The content of '
            '`$filename` in your package should primarily contain characters used in English.',
      ));
    }

    return issues;
  }

  Future<Subsection> checkPubspec() async {
    final issues = <Issue>[];
    if (pubspec.hasUnknownSdks) {
      issues.add(Issue('Unknown SDKs in `pubspec.yaml`.',
          span: tryGetSpanFromYamlMap(
              pubspec.environment, pubspec.unknownSdks.first),
          suggestion: 'The following unknown SDKs are in `pubspec.yaml`:\n'
              '`${pubspec.unknownSdks}`.\n\n'
              '`pana` doesn’t recognize them; please remove the `sdk` entry.'));
    }
    issues.addAll(await findUrlIssues('homepage', 'Homepage URL',
        isRequired: pubspec.repository == null));
    issues.addAll(await findUrlIssues('repository', 'Repository URL',
        isRequired: pubspec.homepage == null));
    issues.addAll(await findUrlIssues('documentation', 'Documentation URL'));
    issues.addAll(await findUrlIssues('issue_tracker', 'Issue tracker URL'));
    final gitDependencies =
        pubspec.dependencies.entries.where((e) => e.value is GitDependency);
    if (gitDependencies.isNotEmpty) {
      issues.add(Issue(
        'The package has a git dependency.',
        span: tryGetSpanFromYamlMap(pubspec.originalYaml['dependencies'] as Map,
            gitDependencies.first.key),
        suggestion: "The pub site doesn't allow git dependencies.",
      ));
    }

    if (pubspec.usesOldFlutterPluginFormat) {
      issues.add(
        Issue(
          'Flutter plugin descriptor uses old format.',
          span: tryGetSpanFromYamlMap(
              pubspec.originalYaml['flutter'] as Map, 'plugin'),
          suggestion:
              'The flutter.plugin.{androidPackage,iosPrefix,pluginClass} keys are '
              'deprecated. Consider using the flutter.plugin.platforms key '
              'introduced in Flutter 1.10.0\n\n See $_pluginDocsUrl',
        ),
      );
    }

    if (pubspec.shouldWarnDart2Constraint) {
      issues.add(
        Issue(
          "Sdk-constraint doesn't allow future stable dart 2.x releases",
          span: tryGetSpanFromYamlMap(
            pubspec.environment,
            'sdk',
          ),
        ),
      );
    }

    // Checking the length of description.
    final description = pubspec.description?.trim();
    final span = tryGetSpanFromYamlMap(pubspec.originalYaml, 'description');
    if (description == null || description.isEmpty) {
      issues.add(
        Issue(
          'Add `description` in `pubspec.yaml`.',
          span: span,
          suggestion:
              'The description gives users information about the features of your '
              'package and why it is relevant to their query. We recommend a '
              'description length of 60 to 180 characters.',
        ),
      );
    } else if (description.length < 60) {
      issues.add(
        Issue('The package description is too short.',
            span: span,
            suggestion:
                'Add more detail to the `description` field of `pubspec.yaml`. Use 60 to 180 '
                'characters to describe the package, what it does, and its target use case.'),
      );
    } else if (description.length > 180) {
      issues.add(
        Issue('The package description is too long.',
            span: span,
            suggestion:
                'Search engines display only the first part of the description. '
                "Try to keep the value of the `description` field in your package's "
                '`pubspec.yaml` file between 60 and 180 characters.'),
      );
    }

    // characters in description
    if (nonAsciiRuneRatio(description) > 0.1) {
      issues.add(Issue(
        'The package description contains too many non-ASCII characters.',
        span: span,
        suggestion:
            'The site uses English as its primary language. The content of the '
            "`description` field in your package's `pubspec.yaml` should "
            'primarily contain characters used in English.',
      ));
    }

    issues.addAll(findFileSizeIssues(File(p.join(packageDir, 'pubspec.yaml')),
        limitInKB: 32));

    final status = issues.isEmpty ? ReportStatus.passed : ReportStatus.failed;
    final points = issues.isEmpty ? 10 : 0;
    return Subsection(
      'Provide a valid `pubspec.yaml`',
      issues,
      points,
      10,
      status,
    );
  }

  Future<Subsection> checkAsset(
    String filename,
    String missingSuggestion,
  ) async {
    final fullPath = p.join(packageDir, filename);
    final file = File(fullPath);
    final issues = <Issue>[];

    if (!file.existsSync()) {
      issues.add(
        Issue('No `$filename` found.', suggestion: missingSuggestion),
      );
    } else {
      issues.addAll(
          findFileSizeIssues(file, missingSuggestion: missingSuggestion));
      issues.addAll(await findMarkdownIssues(file));
    }
    final status = issues.isEmpty ? ReportStatus.passed : ReportStatus.failed;
    final points = issues.isEmpty ? 5 : 0;
    return Subsection(
      'Provide a valid `$filename`',
      issues,
      points,
      5,
      status,
    );
  }

  final readmeSubsection = await checkAsset(
    'README.md',
    'The `README.md` file should inform others about your project, what it does, and how they can use it. '
        'See: the [example](https://raw.githubusercontent.com/dart-lang/stagehand/master/templates/package-simple/README.md) generated by `stagehand`.',
  );
  final changelogSubsection = await checkAsset(
    'CHANGELOG.md',
    'Changelog entries help developers follow the progress of your package. '
        'See the [example](https://raw.githubusercontent.com/dart-lang/stagehand/master/templates/package-simple/CHANGELOG.md) generated by `stagehand`.',
  );
  final pubspecSection = await checkPubspec();
  final subsections = [pubspecSection, readmeSubsection, changelogSubsection];
  return makeSection(
    id: ReportSectionId.convention,
    title: 'Follow Dart file conventions',
    maxPoints: 20,
    subsections: subsections,
    basePath: packageDir,
    maxIssues: 10,
  );
}